diff --git a/docs/Images/Screens/BadDriverZone.png b/docs/Images/Screens/BadDriverZone.png new file mode 100644 index 0000000..6bbba35 Binary files /dev/null and b/docs/Images/Screens/BadDriverZone.png differ diff --git a/docs/Images/Screens/BadOcr.png b/docs/Images/Screens/BadOcr.png new file mode 100644 index 0000000..369a1f6 Binary files /dev/null and b/docs/Images/Screens/BadOcr.png differ diff --git a/docs/index.md b/docs/index.md index ed033d2..c9e9935 100644 --- a/docs/index.md +++ b/docs/index.md @@ -28,17 +28,22 @@ Le but du projet est donc de fournir un outil qui hiérarchise et affiche diffé ### Abstract -Track Trends is a Formula 1 data and analysis tool. +Track Trends is a Formula 1 data is a tool that displays and interpret data. -To understand everything, a little bit of context. In my free time I have multiple activities and one is to be the Live Ticker F1 for the local journal "20 minutes". to help me in this work I'm currently using the F1TV to which I'm currently subscribed because it provides me with a better video feed with better commentary than the ones from the RTS (in my opinion) but also because it gives me access to a very important video feed : the data channel +To understand everything,first ,a little bit of context. In my free time I have multiple activities and one is to be the Live Ticker F1 for the local journal "20 minutes" (Owned by Tamedia). to help me in this work I'm currently using the F1TV to which I'm currently subscribed because it provides me with a better video feed with better commentary than the ones from the RTS (in my opinion) but also because it gives me access to a very important video feed : the data channel -See the screenshot above to see what it looks like. +You can see in the chapter above an example of the F1TV DATA CHANNEL. -[note: It's a pretty HTML table but a full on video feed that contains a table (probably, so you can't access data directly)] +[Note : Even tough it looks like a pretty HTML table on wich you could easely get infos... Its not. Its a video feed] You can see a lot of data all well and good BUT! All the data is displayed the same in a big table which make it really hard to read totally in a hurry, which means that I miss a lot of useful information. -The point of the project then is to provide with a tool that can display those data by taking into account their relevance. That would help me not miss any and provide a better commentary by never missing out battles, and be able to better write with the time I saved by using it. +The point of the project then is to provide a tool that can display those data by taking into account their relevance. +So for example a driver that is 10s away from everyone and that is doing some normal lapTimes will be less displayed or even not displayed at all so I can focus on the drivers that are battling each others. + +This tool would help me not miss the battles and details that are happening in the back and therefore not being broadcasted on TV.And it could be a usefull tool for anyone who wants a better insight of how the race is going by looking at the data. + +This kind of project already exists in the form of the AWS tool "F1 Insight" but it is not avaible to the public. We can only see some of its predictions (that are trash) and data dumps in the live feed when the TV directors feel like it. ### Description du besoin diff --git a/docs/jdb.md b/docs/jdb.md index c285f02..6c53792 100644 --- a/docs/jdb.md +++ b/docs/jdb.md @@ -2187,4 +2187,434 @@ Bon au final j'ai quand même changé mon poster Mais je suis trop attaché à l'ancien concept alors je vais plutôt utiliser ca : -!["Poster V9"](./Images/Figma/PosterV9.png) \ No newline at end of file +!["Poster V9"](./Images/Figma/PosterV9.png) + +Je pense que cette version est meilleure même si elle est encore plus en bordel par ce que le texte permet de se faire une meilleure idée de l'utilisé de chaque partie. + +## Mercredi 10 Mai 2023 + +Bon hier je n'ai pas eu le temps de finir la documentation de la recupèration d'images et de la calibration. Il faudra donc que je repasse un coup dessus en fin de semaine je pense. + +Mais la j'aimerais avancer sur la mise en commun du projet, comme la configuration fonctionne plutôt pas mal je pense que je vais juste vite fait aller commenter les methodes qui ne le sont pas encore et ensuite je vais passer à l'implémentation de l'OCR. + +Je suis presque certain que l'OCR va avoir besoin de plus de règlages mais bon on verra bien. + +Je me rend compte en commentant que la methode de load serait plus efficace avec un tout petit peu plus d'infos de la part du JSON. J'aurais pu ajouter l'offset entre chaque Driver Zone pour eviter un lèger drift lors de la reconstruction. Mais bon rien de grave donc je pense que je vais le laisser comme ca pour le moment à moins que ca me pose soucis plus tard. + +J'ai eu quelques soucis avec les images en 4K. Du coup j'ai descendu les variables d'environnement à 1920x1080 + +En fait il y a parfois un soucis un peu pénible avec l'OCR. + +Parfois pour un temps comme ci dessous: + +!["1:45.140"](./Images/Screens/BadOcr.png) + +Le programme ne va pas bien comprendre les ponctuations et il va donner : `1115140` + +La il y a deux problèmes... Le 1:xx.xxx est compris comme 11xxxxx et le 4 s'est transformé en 1... + +J'ai créé ce "petit" bout de code pour gèrer les fois ou les '.' et les ':' ont mal été interprêtés + +```Csharp +if (rawNumbers.Count == 1) + { + //If this code is used it means that its bad ... + //The methods that comes are really not that great and are juste quick fixes + try + { + result = Convert.ToInt32(rawNumbers[0]); + + switch (windowType) + { + case OcrImage.WindowType.Sector: + //The usual sector is in this form : 33.456 + if (rawNumbers[0].Length == 6) + { + //The '.' has been understood like a number + result = 0; + result += Convert.ToInt32(rawNumbers[0][0] + rawNumbers[0][1]) * 1000; + result += Convert.ToInt32(rawNumbers[0][3] + rawNumbers[0][4] + rawNumbers[0][5]); + } + if (rawNumbers[0].Length == 5) + { + //The '.' has been overlooked + result = 0; + result += Convert.ToInt32(rawNumbers[0][0] + rawNumbers[0][1]) * 1000; + result += Convert.ToInt32(rawNumbers[0][2] + rawNumbers[0][3] + rawNumbers[0][4]); + } + break; + case OcrImage.WindowType.LapTime: + //The usual Lap time is in this form : 1:45:345 + if (rawNumbers[0].Length == 6) + { + //The '.' and ':' have been overlooked + //I Know Im skipping the cases where there are more than 9 minuts but it happens so rarely that... we dont care + result = 0; + result += Convert.ToInt32(rawNumbers[0][0]) * 60000; + result += Convert.ToInt32(rawNumbers[0][1] + rawNumbers[0][2]) * 1000; + result += Convert.ToInt32(rawNumbers[0][3] + rawNumbers[0][4] + rawNumbers[0][5]); + } + if (rawNumbers[0].Length == 7) + { + //There is two possibilities + //Either 1:45.140 has been interpreted as 1145.10 or 1:451140. We will assume its the first one + result = 0; + result += Convert.ToInt32(rawNumbers[0][0]) * 60000; + result += Convert.ToInt32(rawNumbers[0][2] + rawNumbers[0][3]) * 1000; + result += Convert.ToInt32(rawNumbers[0][4] + rawNumbers[0][5] + rawNumbers[0][6]); + } + break; + case OcrImage.WindowType.Gap: + //The usual Gap is in this form : + 34.567 + if (rawNumbers[0].Length == 5) + { + //The '.' has been overlooked + result += Convert.ToInt32(rawNumbers[0][0] + rawNumbers[0][1]) * 1000; + result += Convert.ToInt32(rawNumbers[0][2] + rawNumbers[0][3] + rawNumbers[0][4]); + } + break; + } + if (rawNumbers[0].Length > 6) + { + //The number definitely has been interpreted wrong + } + } + catch + { + //It can be because the input is empty or because its the LEADER bracket + result = 0; + } + } + else + { + //Auuuugh + result = 0; + } +``` + +```Csharp +ConfigFile = "./Presets/Clean_2023.json"; +string gpUrl = "https://f1tv.formula1.com/detail/1000006688/2023-azerbaijan-grand-prix?action=play"; +``` + +Bon je n'arrive pas à faire fonctionner l'OCR sans tout faire crash à chaque fois. Je vais abandonner le travail de la journée pour revenir au point initial... C'est très frustrant mais bon je ne vois pas comment faire mieux. Rien ne marche alors qu'avant ca marchant super sur le projet OCR normal. + +Va savoir pourquoi même comme ca, impossible de faire marcher l'OCR. Il y a un soucis au niveau de l'ASYNC qui me fait crash tout le temps en me disant qu'un objet est deja en train d'être utilisé. Ca marchait nikel dans mes premières version je ne vois pas pourquoi ca pête maintenant. + +Je pense que je vois à peu près le soucis. + +```Csharp +public virtual async Task Decode(List driverList) + { + int sectorCount = 0; + DriverData result = new DriverData(); + Parallel.ForEach(Windows, async w => + { + // A switch would be prettier but I dont think its supported in this C# version + if (w is DriverNameWindow) + result.Name = (string)await (w as DriverNameWindow).DecodePng(driverList); + if (w is DriverDrsWindow) + result.DRS = (bool)await (w as DriverDrsWindow).DecodePng(); + if (w is DriverGapToLeaderWindow) + result.GapToLeader = (int)await (w as DriverGapToLeaderWindow).DecodePng(); + if (w is DriverLapTimeWindow) + result.LapTime = (int)await (w as DriverLapTimeWindow).DecodePng(); + if (w is DriverPositionWindow) + result.Position = (int)await (w as DriverPositionWindow).DecodePng(); + if (w is DriverSectorWindow) + { + sectorCount++; + if (sectorCount == 1) + result.Sector1 = (int)await (w as DriverSectorWindow).DecodePng(); + if (sectorCount == 2) + result.Sector2 = (int)await (w as DriverSectorWindow).DecodePng(); + if (sectorCount == 3) + result.Sector3 = (int)await (w as DriverSectorWindow).DecodePng(); + } + if (w is DriverTyresWindow) + result.CurrentTyre = (Tyre)await (w as DriverTyresWindow).DecodePng(); + }); + return result; + } +``` + +Ca c'est ma methode de decoding de chaque Driver Zone. Le message d'erreur me parle d'une windowImage quand il dit qu'un objet est déja utilisé. Ma conjecture c'est que en essayant de faire toutes les windows en même temps. Elles veulent parfois accèder à l'image principale en même temps. Ce qui evidemment pose problème. Je pense que le fix le plus simple serait de faire le traitement sans le parallele quitte à exporter ce fonctionnement sur chaque zone en elle même pour ne pas perdre trop de performances. + +Ok je crois que je vois ou est le soucis. En fait dans cette version du programme c'est toujours la première image qui était juste tout le temps prise et dans la première image on a une partie des chiffres qui est bloquée par l'UI de la fenêtre... lol... + +EN FAIT +J'avais un soucis dans ma gestion des chiffres mal faits. Visiblement parfois quand je ne prenais pas en compte un :, un LapTime etait compris comme un Gap to leader ou un Secteur + +Bon j'en ai tellement marre... Je n'arrive tout simplement PAS à faire fonctionner l'OCR ca crash tout le temps j'en peux plus. + +J'ai tenté de règler les problèmes de mauvaises detections de secteurs et temps au tour qui font crasher l'app : + +```Csharp +if (rawNumbers.Count == 2) + { + //ss:ms + result = (Convert.ToInt32(rawNumbers[0]) * 1000) + Convert.ToInt32(rawNumbers[1]); + + if (result > (60000 + 999)) + { + if (windowType == OcrImage.WindowType.LapTime) + { + result = 0; + result += Convert.ToInt32(rawNumbers[0][0]) * 60000; + result += Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString()) * 1000; + result += Convert.ToInt32(rawNumbers[1]); + } + if (windowType == OcrImage.WindowType.Sector) + { + int seconds = 0; + if (rawNumbers[0].Length == 3) + { + //We have one char that we need to delete + //For no apparent reason im going to delete the first + seconds = Convert.ToInt32(rawNumbers[0][1].ToString() + rawNumbers[0][2].ToString()); + } + else + { + seconds = Convert.ToInt32(rawNumbers[0][0].ToString() + rawNumbers[0][1].ToString()); + } + int ms = Convert.ToInt32(rawNumbers[0][0].ToString() + rawNumbers[0][1].ToString() + rawNumbers[0][2].ToString()); + result = seconds * 1000 + ms; + } + } + } +``` + +Mais toujours impossible de faire fonctionner cette M**** C'est juste infernal. Je pense que je vais encore tout retirer et remplacer par ce que j'ai dans mon projet OCR original. Donc c'est une journée de perdue complêtement... C'est extrêmement frustrant. + +Après des heures de debug j'ai enfin réussi à faire fonctionner le programme de temps en temps. Mais j'ai toujours le soucis que l'image ne veut pas changer alors que je fais tout pour et que l'OCR est nulle à chier du coup... + +## Jeudi 11 Mai 2023 + +Bon après une bonne nuit de sommeil je vais reprendre les choses depuis le début. + +J'ai deux soucis : + +- L'OCR pue du derche +- L'Image que l'on décode ne change pas + +Pour la première partie j'ai ma petite théorie. Je pense que comme je donne des images 4K alors que le feed est en 1080P, il y a déja un genre d'interpolation qui est faite. Je pense donc qu'il faut que j'adapte mon engine pour qu'il fonctionne avec cette résolution. + +Je me suis demandé si ca n'était pas mieux de prendre en compte les deux résolutions pour les pc un peu moins balèzes et j'ai décidé de n'en avoir rien a faire. On verra dans le futur si c'est une feature que je voudrais ajouter mais c'est en dehors du scope du diplôme je pense. + +Pour la seconde partie, je pense qu'il faut que j'aille voir du côte de OCR_Decode et de OCR Tester pour voir comment je faisais. Je dois forcément oublier un truc. + +Bon ca commence mal, quand je vais voir dans le projet OCR_Decode, le changement d'image est exactement le même et il fonctionne alors que de mon côté ca n'est pas le cas. + +Alors deux choses. Je me rend compte que le changement d'images n'a AUCUN effet sur la detection de texte, et seconde chose, le décalage est trop grand entre les windows. Des que le soucis d'image est règlé il va falloir que je change drastiquement ma facon de stocker la config en JSON. Il faut que je conserve les écarts. + +Sinon regardez ce que ca donne quand on arrive au dernier pilote : + +!["Zone de pilote décalée"](./Images/Screens/BadDriverZone.png) + +Je commence à devenir FOU. Je n'arrive pas à changer cette foutue image wtf... J'ai beau tenter par tous les moyens de la changer par une image noire, l'image semble toujours rester celle du départ. + +Bon j'ai enfin trouvé pourquoi et je n'ai pas envie de dire comment j'ai trouvé... Je pense que l'on a tous droit à son petit jardin secret. + +Maintenant ca veut dire que je peux me focus sur le concept important qui est le changement de la création et de la lecture des JSON. + +Voici un exemple de preset JSON : + +```JSON +{ + "Main": { + "x": 40, + "y": 355, + "width": 3784, + "height": 1438, + "Zones": [ + { + "DriverZone": { + "x": 0, + "y": -10, + "width": 3784, + "height": 71, + "Windows": [ + { + "Position": { + "x": 47, + "y": 11, + "width": 72 + }, + "GapToLeader": { + "x": 445, + "y": 13, + "width": 201 + }, + "LapTime": { + "x": 859, + "y": 14, + "width": 221 + }, + "DRS": { + "x": 1094, + "y": 13, + "width": 173 + }, + "Tyres": { + "x": 1270, + "y": 11, + "width": 1452 + }, + "Name": { + "x": 2727, + "y": 11, + "width": 351 + }, + "Sector1": { + "x": 3083, + "y": 10, + "width": 253 + }, + "Sector2": { + "x": 3339, + "y": 14, + "width": 195 + }, + "Sector3": { + "x": 3518, + "y": 14, + "width": 250 + } + } + ] + } + } + ] + }, + "Drivers": [ + "Perez", + "Leclerc", + "Sainz", + "Alonso", + "Stroll", + "Russel", + "Verstappen", + "Zhou", + "Ocon", + "Hulkenberg", + "Hamilton", + "Norris", + "Tsunoda", + "Magnussen", + "Piastri", + "Albon", + "Gasly", + "Sargeant", + "Bottas", + "De Vries" + ] +} +``` + +Je pense que ce qui serait bien ce serait de rajouter un "offsets" qui contienne les 19 écarts restants. + +Bon... la structure de ma fabrication de JSON etait trop confuse je trouve alors je l'ai complêtement refaite. + +J'ai aussi abandonné l'idée de faire un fichier le plus petit possible car au final on s'en fiche et le plus important c'est que toutes les windows et les zones soient aux bons endroits. + +Ca nous fait un fichier d'environs 1300 lignes mais au moins le code pour la serialisation est plutôt clean : + +```Csharp +public void SaveToJson(List drivers, string configName) + { + string JSON = ""; + + JsonObject jsonFileObject = new JsonObject(); + + //Creating the mainZone object + + JsonObject mainZoneObject = new JsonObject(); + mainZoneObject.Add("x",MainZone.Bounds.X); + mainZoneObject.Add("y",MainZone.Bounds.Y); + mainZoneObject.Add("width",MainZone.Bounds.Width); + mainZoneObject.Add("height",MainZone.Bounds.Height); + + JsonArray driverZonesArray = new JsonArray(); + + int DriverID = 0; + foreach (Zone driverZone in MainZone.Zones) + { + DriverID++; + JsonObject driverZoneObject = new JsonObject(); + driverZoneObject.Add("name","Driver"+DriverID); + driverZoneObject.Add("x", driverZone.Bounds.X); + driverZoneObject.Add("y", driverZone.Bounds.Y); + driverZoneObject.Add("width", driverZone.Bounds.Width); + driverZoneObject.Add("height", driverZone.Bounds.Height); + + JsonArray windowsArray = new JsonArray(); + + JsonObject windowObject = new JsonObject(); + foreach (Window window in driverZone.Windows) + { + windowObject.Add(window.Name, new JsonObject { + { "x", window.Bounds.X }, + { "y", window.Bounds.Y }, + { "width", window.Bounds.Width }, + { "height", window.Bounds.Height } + }); + } + windowsArray.Add(windowObject); + + driverZoneObject.Add("Windows",windowsArray); + + driverZonesArray.Add(driverZoneObject); + } + + mainZoneObject.Add("DriverZones",driverZonesArray); + + JsonArray driversArray = new JsonArray(); + + foreach (string driver in drivers) + { + driversArray.Add(driver); + } + + mainZoneObject.Add("Drivers",driversArray); + + jsonFileObject.Add("Main",mainZoneObject); + + JSON = jsonFileObject.ToString(); + + //Saving the file + string path = CONFIGS_FOLDER_NAME + configName; + + if (File.Exists(path + ".json")) + { + //We need to create a new name + int count = 2; + while (File.Exists(path + "_" + count + ".json")) + { + count++; + } + path += "_" + count + ".json"; + } + else + { + path += ".json"; + } + + File.WriteAllText(path, JSON); + } +``` + +Et normalement la lecture devrait être encore plus simple. + +En fait c'était pas beaucoup plus simple mais au moins maintenant ca marche. Je vais pas mettre le code de lecture ici car c'est un peu trop long donc il va falloir me croire sur parole. (Ou aller sur Git) + +Bon bah on est au même endroit qu'hier... + +Bon pour demain le plan de bataille ca va être : + +Changer complêtement la methode "GetTimeFromPng" pour qu'elle prenne en compte toutes les possibilités de bugs et d'oubli de '.' ou de ':' mais pas selon le nombre de blocs mais selon le type de temps que l'on cherche + +Pour le moment je regarde le nombre de blocs et si il y en a deux alors c'est que c'est un temps de secteur. En fait non cela peut aussi être un temps au tour qui a raté un point. + +Il faut que je bosse juste un peu vite fait la dessus et que j'arrête de putain de crasher dès que un truc est pas au bon format. Ensuite quand ca aura arrêté de crasher je vais reprendre l'OCR et voir pourquoi les resultats sont nuls a chier comme ca. + +Et le but c'est que demain soir j'ai une reconnaissance de caractères plus proche de ce que j'avais dans d'autres projets... J'y croit 0 mais bon l'espoir fait vivre comme on dit. diff --git a/site/index.html b/site/index.html index 78bcfaf..26e53e1 100644 --- a/site/index.html +++ b/site/index.html @@ -435,6 +435,16 @@
  • + + Optimisation du programme + +
  • +
  • + + Ethique du projet + +
  • +
  • Améliorations futures @@ -894,6 +904,16 @@
  • + + Optimisation du programme + +
  • +
  • + + Ethique du projet + +
  • +
  • Améliorations futures @@ -931,12 +951,15 @@

    Sauf que toutes les informations sont étalées pêle-mêle sans hiérarchie ce qui fait que cela me prendrait trop de temps de tout déchiffrer à chaque fois, ce qui me fait rater des choses intéressantes.

    Le but du projet est donc de fournir un outil qui hiérarchise et affiche différemment les données pour faciliter leur lecture et me permettre de faire de meilleurs commentaires.

    Abstract

    -

    Track Trends is a Formula 1 data and analysis tool.

    -

    To understand everything, a little bit of context. In my free time I have multiple activities and one is to be the Live Ticker F1 for the local journal "20 minutes". to help me in this work I'm currently using the F1TV to which I'm currently subscribed because it provides me with a better video feed with better commentary than the ones from the RTS (in my opinion) but also because it gives me access to a very important video feed : the data channel

    -

    See the screenshot above to see what it looks like.

    -

    [note: It's a pretty HTML table but a full on video feed that contains a table (probably, so you can't access data directly)]

    +

    Track Trends is a Formula 1 data is a tool that displays and interpret data.

    +

    To understand everything,first ,a little bit of context. In my free time I have multiple activities and one is to be the Live Ticker F1 for the local journal "20 minutes" (Owned by Tamedia). to help me in this work I'm currently using the F1TV to which I'm currently subscribed because it provides me with a better video feed with better commentary than the ones from the RTS (in my opinion) but also because it gives me access to a very important video feed : the data channel

    +

    You can see in the chapter above an example of the F1TV DATA CHANNEL.

    +

    [Note : Even tough it looks like a pretty HTML table on wich you could easely get infos... Its not. Its a video feed]

    You can see a lot of data all well and good BUT! All the data is displayed the same in a big table which make it really hard to read totally in a hurry, which means that I miss a lot of useful information.

    -

    The point of the project then is to provide with a tool that can display those data by taking into account their relevance. That would help me not miss any and provide a better commentary by never missing out battles, and be able to better write with the time I saved by using it.

    +

    The point of the project then is to provide a tool that can display those data by taking into account their relevance. +So for example a driver that is 10s away from everyone and that is doing some normal lapTimes will be less displayed or even not displayed at all so I can focus on the drivers that are battling each others.

    +

    This tool would help me not miss the battles and details that are happening in the back and therefore not being broadcasted on TV.And it could be a usefull tool for anyone who wants a better insight of how the race is going by looking at the data.

    +

    This kind of project already exists in the form of the AWS tool "F1 Insight" but it is not avaible to the public. We can only see some of its predictions (that are trash) and data dumps in the live feed when the TV directors feel like it.

    Description du besoin

    Comme expliqué dans le résumé, je suis Live Ticker F1. Mais pour mieux comprendre le besoin que j'ai, je pense qu'il est pertinent de comprendre comment je travaille.

    Pendant un Grand Prix de Formule 1 j'ai plusieurs tâches à effectuer :

    @@ -986,8 +1009,8 @@ La raison la plus probable étant qu'Amazon avec son service AWS propose exactem

    Mais comme je possède un abonnement premium ++ à la F1TV, j'ai accès pour chaque grand prix à un flux vidéo nommé : DATA F1 CHANNEL

    Qui ressemble à ça :

    - -
    "Data channel exemple"
    + +
    "Exemple de la Data Channel"

    Donc la seule façon que je vois de récupérer ces données est de les prendre directement sur ce feed.

    Même si le but final de l'application est de faire pleins de choses super avec les datas, le gros du projet va surtout être la récupération des données et leur stockage.

    @@ -1017,8 +1040,8 @@ La raison la plus probable étant qu'Amazon avec son service AWS propose exactem

    Voici un exemple d'interface possible pour une page :

    - -
    "Proto"
    + +
    "Protype de l'app fait sur Figma"

    Cas d'utilisation


    @@ -1137,21 +1160,21 @@ La raison la plus probable étant qu'Amazon avec son service AWS propose exactem

    Pour rappel, Amazon héberge directement le site de la F1TV et possède les droits sur les données de la F1. C'est sous le nom de AWS (le service d'hébergement d'Amazon) que la firme apparait en tant que sponsor.

    On peut voir ce nom apparaître assez souvent quand on regarde un Grand Prix car comme ils ont la main-mise sur les données ils peuvent insèrer des bandeaux d'informations sur le flux public sur ce qu'il se passe voir même faire des prédictions (Bien qu'un peu bancales)

    - -
    "AWS example 1"
    + +
    "Exemple insertion AWS en GP"

    Ce service s'appelle F1 Insights (Oui c'est un meilleur nom de projet que F1 Companion mais bon) et c'est, je pense, la raison pour laquelle on ne voit aucune API publique qui permette de correctement se renseigner en donées en direct pendant un Grand Prix. Ils ont du dégotter un juteux contrat pour s'occuper de toute l'infrastructure digitale de la F1 (du moins publique) en échange d'une exclusivité totale sur certaines choses comme les Data

    - -
    "AWS example 2"
    + +
    "Exemple data d'AWS"

    Evidemment je ne fais que conjecturer et ce que j'ai dit n'est pas à prendre au pied de la lettre mais c'est une explication possible je pense de pourquoi il est si difficile de trouver des données sur la F1 facilement en temps réel.

    Il existe bien quelques API un peu bancales publiques, mais le problème c'est qu'elles ne sont vraiment pas suffisante et je ne peux pas leur faire confiance quand je commente. Ce qu'il m'aurait fallut c'est une API publique et officielle qui me permette d'être sur que les données sont les bonnes et qu'elles arrivent le plus vite possible.

    On pourrait croire que c'est impossible car cela n'existe pas comme je l'ai dit MAIS ! Ce n'est pas complêtement vrai. En effet depuis que je possède un abonnement à la F1TV, il existe une source d'informations très précieuse qui m'aide énormément dans mon quotidien de commentateur de Formule 1. La "DATA CHANNEL".

    La Data Channel est une page de la F1TV qui permet, pour chaque Grand Prix, de visualiser, sous la forme d'un flux vidéo, différentes informations capitales sur la course.

    - -
    "Data channel example"
    + +
    "Exemple de Data Channel"

    Le problème, c'est que comme je viens de le dire, ces données ne sont pas accessibles comme un tableau HTML ou un flux RSS ou un tableau JSON. C'est un flux vidéo. Il faut savoir qu'entretenir une diffusion de flux vidéo en 1080P pendant deux heures accessible par des milliers d'abonnés est EXTRÊMENT cher surtout quand on le compare à simplement afficher les données dans un tableau. Ce qui veut dire que ce choix est délibéré et a un sens au niveau économique. @@ -1192,8 +1215,8 @@ Je pense donc que c'est justement pour éviter que des petits malins puissent si

    Cependant Firefox de pas sa nature Open Source utilise "OpenH264" pour lire ces mêmes flux soumis à des DRM et OpenH264 n'implémente pas les mêmes restrictions.

    Sauf que Firefox n'est pas aussi facilement émulé que chrome et cela réduit notre choix de librairies à ... Une seule... Qui est Selenium. (Il existe aussi Pupetteer C# mais j'ai rencontré énormément de soucis avec cette dernière dès que je voulais lancer une vidéo)

    - -
    "Firefox Developper logo"
    + +
    "Firefox dev logo"

    {: style="height:150px;width:150px"}

    Mais même si la documentation est plutôt maigre parfois, c'est une bonne librairie qui permet de très bien contrôler une instance de chrome ou de Firefox.

    @@ -1260,8 +1283,8 @@ fullScreenButton.Click();

    Si on regarde de loin on peut se dire que la structure est plutôt simple mais c'est loin d'être le cas. On peut y voir au moins 4 zones contenant de l'information dans un format différent.

    - -
    "Main zones"
    + +
    "Zones principales"

    Dans l'exemple ci dessus on peut voir 3 zones mais on aurait également pu comprendre la zone de position des pilotes autour du circuit pour faire 4.

    Ces 4 zones sont très différentes et contiennent d'autres informations. Pour ce travail de diplôme je ne m'occupe que de la zone principale. Mais je pense que le titre et les infos de circuit ne prendrait pas tant de temps que ca à implémenter.

    @@ -1289,39 +1312,39 @@ On peut y voir au moins 4 zones contenant de l'information dans un format diffé

    Voila donc un petit diagramme qui montre le découpage du programme :

    - -
    "Diagramme zones"
    + +
    "Diagramme explicatif de l'architecture des zones"

    Pour visualiser encore un peu mieux comment ce découpage prend forme voici ce que chaque zone et Window contient.

    Main Zone :

    - -
    "Main zone"
    + +
    "Exemple zone principale"

    Driver Zone :

    - -
    "Driver zone"
    + +
    "Exemple zone de pilote"

    Driver Position Window :

    - -
    "Driver position Window"
    + +
    "Exemple de fenêtre de position"

    Driver name Window :

    - -
    "Driver name window"
    + +
    "Exemple de fenêtre de nom"

    Driver LapTime Window :

    - -
    "Driver Laptime window"
    + +
    "Exemple de fenêtre de temps au tour"

    Driver Tyre Window :

    - -
    "Driver tyre window"
    + +
    "Exemple de fenêtre pneus"

    Il existe d'autres types de Window mais ce sont les principaux.

    On se rend assez facilement compte que chacunes de ces windows va avoir besoin d'un traitement spécifique car la manière de reconnaitre le pneu utilisé et le temps au tour ne peut pas être la même.

    @@ -1338,29 +1361,29 @@ On peut y voir au moins 4 zones contenant de l'information dans un format diffé

    Exemple :

    Prenons le chiffre 9. Dans l'image il peut être représenté de cette manière :

    - -
    "Bad9Exemple"
    + +
    "Exemple de chiffre avant post traitement"

    On peut voir qu'il est flou, pour nous cela ne pose pas de problème et je pense que à peu près nimporte qui peut dire que c'est un 9.

    Cependant comme les contours sont flous et même si on essaie de retirer le background :

    - -
    "Aliased 9"
    + +
    "9 avec anti aliasing"

    On voit que le 9 n'est pas clairement définit. En effet on pourrait le comprendre comme :

    - -
    "First contour"
    + +
    "Premier exemple de contours"

    Ou comme :

    - -
    "Second contour"
    + +
    "Second exemple de contours"

    Voire même simplement comme :

    - -
    "Big contour"
    + +
    "Exemple de coutour généreux"

    Et on se rend bien compte que les performances de detection ne sont pas les mêmes dans ces trois cas.

    Il faut donc faire un certain post traitement des images pour supprimer les éléments parasites, les couleurs, et augmenter la visibilité des contours importants.

    @@ -1372,8 +1395,8 @@ On peut y voir au moins 4 zones contenant de l'information dans un format diffé

    Cette reconnaissance concerne donc des lettres qui font des mots ou des noms.

    Voici un exemple de la WINDOW nom de pilote en entrée :

    - -
    "Exemple raw"
    + +
    "Exemple texte cru"

    Ce texte peut paraitre bon, cependant quand on le lance dans Tesseract, il ne va pas toujours donner un résultat parfait. Il faut aussi savoir qu'il y a des noms pas mal plus pénibles que Tesseract a plus de mal à reconnaitres, soit à cause des lettres utilisées, soit car le nom est un nom d'une autre région et qui ne veut rien dire en anglais ce qui empêche l'utilisation de dictionnaire (Ex : Tsunoda est un nom japonais et parfois il est difficile pour Tesseract de le reconnaitre car si une lettre pose problême il ne peut pas trouver de contexte qui puisse l'aider).

    Donc pour le rendre plus facilement lisible et augmenter les chances que toutes les lettres soient découvertes, voici les étapes que j'ai mis en place.

    @@ -1384,18 +1407,18 @@ On peut y voir au moins 4 zones contenant de l'information dans un format diffé

    2 : Je fais un Treshhold de 165 car avec moins le texte parfois prend trop du background et avec plus les lettres sont trop fines.

    - -
    "Treshold"
    + +
    "Texte après Treshold"

    3 : Je fais un Resize de l'image pour avoir une meilleure résolution et permettre une meilleure détection. J'augmente la hauteur et la largeur par un facteur 2. J'ai trouvé cette valeur suffisante et aller plus haut consomme beaucoup de ressources.

    - -
    "Resize"
    + +
    "Texte après Resize"

    4: Je fais une très rapide Dilatation du texte pour retirer le flou amené par la methode de Resize. Je n'utilise qu'une valeur de 1 car je ne veux pas trop changer comment le texte est modelé je veux juste retirer le flou.

    - -
    "Dilatation"
    + +
    "Texte après Dilatation"

    Explication des methodes précises plus bas

    Voila pour ce qui est du post processing. Je ne dis pas que ce sont les meilleurs paramêtres possibles mais dans mes tests ce sont ceux qui ont le mieux marchés.

    @@ -1424,36 +1447,36 @@ On peut y voir au moins 4 zones contenant de l'information dans un format diffé

    Cependant, tous ces temps possèdent le même type de post-traitement avant d'être envoyés à Tesseract.

    Voici un exemple de temps au tour avant toute transformation :

    - -
    "Lap time"
    + +
    "Temps au tour avant traitement"

    On peut avoir l'impression que ce texte est tout à fait lisible et facile à décoder surtout quand on le voit de loin comme ca. Cependant, il faut imaginer que ces chiffres font 13 pixels de haut en comptant le flou et comme expliqué plus haut ce flou dans ces echelles est terrible.

    - -
    "Lap time"
    + +
    "Temps au tour zoomé"

    Si on donne cette image à Tesseract, les '3' deviennent des '9', des '9' deviennent des '8', des '2' deviennent eux aussi des '9', le tout parfois inversement et de manière complêtement imprévisible. Ca n'est simplement pas utilisable.

    Cette partie est un peu plus complexe car si la detection n'est pas fiable les chiffres sont simplement inutilisables. Si à tout moment un temps au tour de 1:39.106 devient 1:32.108 c'est juste pas possible.

    Voici donc les étapes de post-traitement que j'ai mis en place pour leur détection :

    1: J'applique un Treshold de 185 pour enlever les ambiguités d'alisaising et avoir une image en noir et blanc claire. La valeur de 185 est assez élevée car le but est de vraiment garder uniquement les contours. Comme les chiffres se ressemlent beaucoup plu que les lettres, il faut tenter le plus possible de conserver leur formes spécifiques. Je me suis rendu compte que cette valeur était une de celles qui marchent le mieux.

    - -
    "Treshold"
    + +
    "Temps au tour après Treshold"

    2: J'applique un Resize de 2 pour augmenter la résolution des chiffres et permettre une meilleure détection. Le but est d'avoir plus de pixels et donc de permettre à Tesseract de mieux utiliser ses matrices de convolution.

    - -
    "Resize"
    + +
    "Temps au tour après Resize"
    -

    3: Comme le Resize amène du flou, j'utilise une methode de Dilatation qui me permet de retirer ce flou et de remplir un peu plus certaines parties qui ont été un peu laissée par le Resize*;

    +

    3: Comme le Resize amène du flou, j'utilise une methode de Dilatation qui me permet de retirer ce flou et de remplir un peu plus certaines parties qui ont été un peu laissée par le Resize;

    - -
    "Dilatation"
    + +
    "Temps au tour après Dilatation"

    4: Contrairement aux mots plus haut, la rondeur ajoutée par la dilatation n'est pas vraiment désirée. En effet, elle peut rendre confuse certains chiffres et empêcher Tesseract de bien trouver le chiffre. Alors j'applique une Erosion qui me permet de contrecarrer en partie les rondeurs ajoutées par la dilatation et retrouver des chiffres bien formées. Pour l'Erosion et la Dilatation j'ai utilisé une valeur de 1 car je ne voulais pas trop changer les chiffres.

    - -
    "Erode"
    + +
    "Temps au tour après Erosion"

    Explication des methodes précises plus bas

    Et avec ce post processing on retrouve de plutôts bon résultats qui demandent peu de traitement.

    @@ -1485,31 +1508,31 @@ On peut y voir au moins 4 zones contenant de l'information dans un format diffé
  • Les pneus pluie
  • - -
    "Tyres"
    + +
    "Gamme de pneus Pirelli"

    Les trois premiers pneus sont des pneus faits pour piste sèche, le pneu intermédiaire pour piste humide et le neu pluie pour la pluie.

    Chaque pneu a sa durée de vie et son niveau de performance propre mais je ne vais pas rentrer dans le détail ici. Tout ce qu'il faut savoir ce que savoir sur quel pneu chaque pilote est et depuis combien de temps il les chausse est une information très importante.

    Chaque pneu a une couleur donnée qui permet de les différencier.

    Voici un exemple de ce à quoi une WINDOW de pneus peut ressembler :

    - -
    "Exemple 1"
    + +
    "Exemple zone pneus 1"

    Mais cette zone peut aussi ressembler à ca :

    - -
    "Exemple 2"
    + +
    "Exemple zone pneus 2"

    Mais aussi à ca :

    - -
    "Exemple 3"
    + +
    "Exemple zone pneus 3"

    Voire même ca :

    - -
    "Exemple 4"
    + +
    "Exemple zone pneus 4"

    Je pense que vous pouvez tout de suite comprendre la difficulté que représente la tâche de récupèration de données à partir de cette image.

    En gros le fonctionnement de cette zone d'information est assez simple.

    @@ -1526,13 +1549,13 @@ On peut y voir au moins 4 zones contenant de l'information dans un format diffé

    Ensuite après avoir trouvé le premier obstacle, je récupère une zone qui doit englober le cercle.

    Voici un exemple avec cette image en entrée :

    - -
    "Full zone"
    + +
    "Zone complête"

    Elle est automatiquement coupée de cette facon :

    - -
    "Cropped zone"
    + +
    "Zone coupée automatiquement"

    Cela me permet d'isoler uniquement ce qui m'intéresse ce qui est très pratique pour Tesseract et pour la detection de couleur.

    Ensuite avec cette image je peux commencer le processus de reconnaissance.

    @@ -1541,38 +1564,38 @@ On peut y voir au moins 4 zones contenant de l'information dans un format diffé

    Il y a cinq couleurs des pneus possibles :

    "#ff0000" pneu tendre/soft

    - -
    "Soft tyre color"
    + +
    "Couleur d'un pneu tendre"

    "#f5bf00" pneu medium

    - -
    "medium tyre color"
    + +
    "Couleur d'un pneu medium"

    "#a4a5a8" pneu dur/hard

    - -
    "Hard tyre color"
    + +
    "Couleur d'un pneu dur"

    "#00a42e" pneu inter

    - -
    "Inter tyre color"
    + +
    "Couleur d'un pneu intermédiaire"

    "#2760a6" pneu pluie/wet

    - -
    "Wet tyre color"
    + +
    "Couleur d'un pneu pluie"

    Ce qui est pratique c'est que même dans les cas ou il n'y a pas beaucoup de couleur comme celui la :

    - -
    "Hard tyre but only the letter"
    + +
    "Pneu dur avec 0 tours"

    On arrive à une couleur moyenne de :

    - -
    "The average color from the picture above"
    + +
    "Couleur moyenne de l'image ci dessus après soustraction du background"

    Et il est donc assez facile de determiner le type de pneu en question.

    Attention, les résultats peuvent être très vite dérangés par la couleur du pneu précédent si le découpage de la fenêtre n'a pas été assez précis.

    @@ -1582,25 +1605,25 @@ On peut y voir au moins 4 zones contenant de l'information dans un format diffé

    Il faut donc retirer le background et ensuite la couleur. Sauf que comme le chiffre est de la couleur du background, si on retire le background et ensuite la couleur il ne reste plus rien. Il faut donc retirer le background AUTOUR du rond, et ensuite si on retire la couleur il devrait rester le chiffre sur fond blanc.

    Pour se faire, j'ai tiré des traits depuis les bords de l'image jusqu'à ce qu'ils rencontrent le rond. Ensuite je retire tous les pixels entre le rond et les bords de l'image ce qui nous donne ceci :

    - -
    "No outer background"
    + +
    "Zone pneu avec le background en moins"

    Ensuite on peu retirer les pixels qui ont une valeur dans un channel RGB plus haute qu'un certain seuil :

    - -
    "Only digit"
    + +
    "Zone avec le reste des couleurs supprimmées"

    Et la on a ce que l'on veut !

    A partir de la c'est les filtres que l'on connait qui sont utilisés pour en faire une image plus facile à utiliser par Tesseract.

    1 : On effectue un Resize de facteur 4 (oui c'est beaucoup mais en même temps le chiffre est vraiment petit à la base) qui permet d'avoir une image d'une bien meilleure résolution.

    - -
    "Filter 1"
    + +
    "Filtre 1"

    2: On fait une Dilatation de facteur 1 pour retirer tout le flou de l'image pour aider Tesseract

    - -
    "Result"
    + +
    "Resultat"

    Et on a un chiffre qui est utilisable par Tesseract !

    Explication des methodes précises plus bas

    @@ -1640,23 +1663,23 @@ C'est une étape très importante pour l'OCR car elle permet (si bien faite) d'i

    Cette methode sert à augmenter la résolution d'une image pour améliorer la précision de l'algorythme de Tesseract. En effet, avec trop peu de pixels, la matrice de convolution n'est pas toujours aussi efficace.

    Il ne faut pas confondre cette methode d'augmentation de la taille avec une simple interpolation. En effet une augmentation de taille interpolée ne vas pas vraiment changer la résolution, l'image sera toujours aussi pixelisée, seulement, les pixels seront composées de plus de pixels comme dans l'exemple ci dessous :

    - -
    "Interpolation exemple"
    + +
    "Exemple d'interpolation linéaire"

    Dans mon projet j'utilise de l'interpolation bicubique qui va créer de l'information pour tenter de combler le vide et produire une image réellement plus grande et avec plus de details mais en ajoutant du flou.

    - -
    "bicubic exemple"
    + +
    "Exemple des différents types d'interpolation"

    Le but est d'aller chercher dans les pixels alentours les couleurs qui sont déja présente et de jouer avec des poids pour tenter de faire une prédiction de ce que ce pixel aurait été si l'image avait plus de detail.

    Voici un exemple assez parlant :

    - -
    "bicubic demonstration"
    + +
    "Exemple interpolation bicubique (avant)"
    - -
    "bicubic demonstration"
    + +
    "Exemple interpolation bicubique (après)"

    On pourrait croire que c'est inutile mais dans le contexte de Tesseract ajouter des détails pour tenter de simuler une meilleure résolution même en créant du flou est intéressant pour mieux remplir la matrice de convolution.

    Mais il est possible de réduire ce flou avec d'autres méthodes également.

    @@ -1680,6 +1703,12 @@ C'est une étape très importante pour l'OCR car elle permet (si bien faite) d'i

    Résumé des difficultés techniques


    [A remplir au fur et à mesure dans la seconde moitié du travail de diplôme]

    +

    Optimisation du programme

    +
    +

    [A remplir à la fin du projet pour parler des différentes methodes d'optimisation]

    +

    Ethique du projet

    +
    +

    [A remplir à la fin du projet pour parler des questions ethiques du projet (Ex: Utilisation potentiellement abusive de la F1Tv ou L'histoire des cookies)]

    Améliorations futures


    [A remplir dans les dernières semaines du travail de diplôme]

    diff --git a/site/pdf/document.pdf b/site/pdf/document.pdf index c8473bb..6334f36 100644 --- a/site/pdf/document.pdf +++ b/site/pdf/document.pdf @@ -3,7 +3,7 @@ 1 0 obj << /Type /Pages -/Kids [ 6 0 R 8 0 R 115 0 R 186 0 R 189 0 R 192 0 R 198 0 R 200 0 R 203 0 R 207 0 R 212 0 R 214 0 R 216 0 R 218 0 R 220 0 R 222 0 R 227 0 R 232 0 R 237 0 R 240 0 R 244 0 R 249 0 R 252 0 R 257 0 R 276 0 R 281 0 R 290 0 R 297 0 R 312 0 R 319 0 R 331 0 R 338 0 R 352 0 R 364 0 R 380 0 R 395 0 R 402 0 R 408 0 R 412 0 R 414 0 R 416 0 R 423 0 R 426 0 R 430 0 R 435 0 R 437 0 R 439 0 R 444 0 R 446 0 R 448 0 R 450 0 R 459 0 R 464 0 R 469 0 R 474 0 R 479 0 R 487 0 R 491 0 R 496 0 R 502 0 R 507 0 R 512 0 R 517 0 R 521 0 R 529 0 R 535 0 R 539 0 R 541 0 R 547 0 R 555 0 R 563 0 R 570 0 R 579 0 R 586 0 R 592 0 R 599 0 R 602 0 R 606 0 R 608 0 R 620 0 R 625 0 R 629 0 R 631 0 R 633 0 R 648 0 R 662 0 R 672 0 R 674 0 R 683 0 R 688 0 R 693 0 R 697 0 R 702 0 R 714 0 R 718 0 R 720 0 R 722 0 R 725 0 R 728 0 R 732 0 R 735 0 R 738 0 R 740 0 R 742 0 R 745 0 R 753 0 R 763 0 R 766 0 R 771 0 R 776 0 R 780 0 R 782 0 R 784 0 R 786 0 R 792 0 R 797 0 R 801 0 R 806 0 R 811 0 R 814 0 R 817 0 R 821 0 R 824 0 R 828 0 R 831 0 R 835 0 R 838 0 R 842 0 R 845 0 R 851 0 R 854 0 R 857 0 R 861 0 R 864 0 R 867 0 R 869 0 R 871 0 R 873 0 R 875 0 R 877 0 R 879 0 R 881 0 R 883 0 R 885 0 R 887 0 R 889 0 R 891 0 R 893 0 R 895 0 R 897 0 R 899 0 R 901 0 R 903 0 R 905 0 R 907 0 R 909 0 R 911 0 R 913 0 R 915 0 R 917 0 R 919 0 R 921 0 R 923 0 R 925 0 R 927 0 R 929 0 R 931 0 R 933 0 R 935 0 R 937 0 R 939 0 R 941 0 R 943 0 R 945 0 R 947 0 R 949 0 R ] +/Kids [ 6 0 R 8 0 R 115 0 R 192 0 R 195 0 R 198 0 R 204 0 R 206 0 R 209 0 R 213 0 R 218 0 R 220 0 R 222 0 R 224 0 R 226 0 R 228 0 R 233 0 R 238 0 R 243 0 R 246 0 R 250 0 R 255 0 R 258 0 R 263 0 R 282 0 R 287 0 R 296 0 R 303 0 R 318 0 R 325 0 R 337 0 R 344 0 R 358 0 R 370 0 R 386 0 R 401 0 R 408 0 R 414 0 R 418 0 R 420 0 R 422 0 R 429 0 R 432 0 R 436 0 R 441 0 R 443 0 R 445 0 R 450 0 R 452 0 R 454 0 R 456 0 R 465 0 R 470 0 R 475 0 R 480 0 R 485 0 R 493 0 R 497 0 R 502 0 R 508 0 R 513 0 R 518 0 R 523 0 R 527 0 R 535 0 R 541 0 R 545 0 R 547 0 R 553 0 R 561 0 R 569 0 R 576 0 R 585 0 R 592 0 R 598 0 R 605 0 R 608 0 R 612 0 R 614 0 R 626 0 R 631 0 R 635 0 R 637 0 R 639 0 R 654 0 R 668 0 R 678 0 R 680 0 R 689 0 R 694 0 R 699 0 R 703 0 R 708 0 R 720 0 R 724 0 R 726 0 R 728 0 R 731 0 R 734 0 R 738 0 R 741 0 R 744 0 R 746 0 R 748 0 R 751 0 R 759 0 R 769 0 R 772 0 R 777 0 R 782 0 R 786 0 R 788 0 R 790 0 R 792 0 R 798 0 R 803 0 R 807 0 R 812 0 R 817 0 R 820 0 R 823 0 R 827 0 R 830 0 R 834 0 R 837 0 R 841 0 R 844 0 R 848 0 R 851 0 R 857 0 R 860 0 R 863 0 R 867 0 R 870 0 R 873 0 R 875 0 R 877 0 R 879 0 R 881 0 R 883 0 R 885 0 R 887 0 R 889 0 R 891 0 R 893 0 R 895 0 R 897 0 R 899 0 R 901 0 R 903 0 R 905 0 R 907 0 R 909 0 R 911 0 R 913 0 R 915 0 R 917 0 R 919 0 R 921 0 R 923 0 R 925 0 R 927 0 R 929 0 R 931 0 R 933 0 R 935 0 R 937 0 R 939 0 R 941 0 R 943 0 R 945 0 R 947 0 R 949 0 R 951 0 R 953 0 R 955 0 R ] /Count 176 >> endobj @@ -19,10 +19,10 @@ endobj << /Type /Catalog /Pages 1 0 R -/Outlines 1053 0 R +/Outlines 1061 0 R /Names << /Dests << -/Names [ (.:) [ 186 0 R /XYZ 37.466457 771.023622 0 ] (.:abstract) [ 192 0 R /XYZ 37.466457 131.212566 0 ] (.:affichage-des-donnees) [ 414 0 R /XYZ 37.466457 710.195622 0 ] (.:ameliorations-futures) [ 414 0 R /XYZ 37.466457 497.358222 0 ] (.:analyse-fonctionnelle) [ 218 0 R /XYZ 37.466457 384.861402 0 ] (.:analyse-organique) [ 218 0 R /XYZ 37.466457 310.794702 0 ] (.:cahier-des-charges) [ 198 0 R /XYZ 37.466457 192.543222 0 ] (.:calibration) [ 237 0 R /XYZ 40.316457 337.595622 0 ] (.:cas-dutilisation) [ 207 0 R /XYZ 37.466457 142.669266 0 ] (.:chiffres) [ 312 0 R /XYZ 40.316457 384.462822 0 ] (.:comment-faire) [ 222 0 R /XYZ 40.316457 208.515666 0 ] (.:conclusion) [ 414 0 R /XYZ 37.466457 423.291522 0 ] (.:controler-le-navigateur) [ 232 0 R /XYZ 40.316457 538.688022 0 ] (.:description-du-besoin) [ 198 0 R /XYZ 37.466457 588.076422 0 ] (.:differences-sur-le-cahier-des-charges) [ 212 0 R /XYZ 37.466457 293.549622 0 ] (.:difficultes-techniques) [ 212 0 R /XYZ 37.466457 479.548422 0 ] (.:drs) [ 395 0 R /XYZ 40.316457 322.168422 0 ] (.:dt) [ 214 0 R /XYZ 40.316457 493.384662 0 ] (.:dt1-creation-du-poster-1) [ 214 0 R /XYZ 40.316457 453.074262 0 ] (.:dt2-documentation-analyse-de-lexistant-2) [ 214 0 R /XYZ 40.316457 370.282902 0 ] (.:dt3-documentation-analyse-organique-5) [ 214 0 R /XYZ 40.316457 299.894742 0 ] (.:dt4-documentation-analyse-fonctionnelle-2) [ 214 0 R /XYZ 40.316457 196.948182 0 ] (.:dt5-documentation-tests-1) [ 216 0 R /XYZ 40.316457 771.023622 0 ] (.:dt6-documentation-reste-2) [ 216 0 R /XYZ 40.316457 720.790662 0 ] (.:filtres-et-methodes-sur-les-images) [ 395 0 R /XYZ 40.316457 284.338662 0 ] (.:filtres-et-traitement) [ 276 0 R /XYZ 40.316457 465.594822 0 ] (.:fonctionnement-general) [ 237 0 R /XYZ 40.316457 131.940822 0 ] (.:interpretation-des-donnees) [ 414 0 R /XYZ 37.466457 771.023622 0 ] (.:introduction) [ 192 0 R /XYZ 37.466457 731.488422 0 ] (.:ocr) [ 237 0 R /XYZ 37.466457 289.533222 0 ] (.:planning-effectif-et-differences) [ 218 0 R /XYZ 37.466457 458.928102 0 ] (.:planning-previsionnel) [ 212 0 R /XYZ 37.466457 219.482922 0 ] (.:pneus) [ 331 0 R /XYZ 40.316457 221.934822 0 ] (.:predictions) [ 414 0 R /XYZ 37.466457 679.781622 0 ] (.:projet) [ 200 0 R /XYZ 37.466457 771.023622 0 ] (.:pt) [ 214 0 R /XYZ 40.316457 625.834422 0 ] (.:pt1-preparation-au-travail-de-diplome-2) [ 214 0 R /XYZ 40.316457 585.524022 0 ] (.:pt1-programmation-recuperation-des-images-3) [ 216 0 R /XYZ 40.316457 608.496102 0 ] (.:pt2-programmation-ocr-5) [ 216 0 R /XYZ 40.316457 455.936742 0 ] (.:pt3-programmation-stockage-et-modele-5) [ 216 0 R /XYZ 40.316457 360.742182 0 ] (.:pt4-programmation-vue-de-lapp-5) [ 216 0 R /XYZ 40.316457 277.950822 0 ] (.:pt5-programmation-mise-en-commun-3) [ 216 0 R /XYZ 40.316457 182.756262 0 ] (.:pt_1) [ 216 0 R /XYZ 40.316457 648.806502 0 ] (.:rapport-track-trends-v10) [ 186 0 R /XYZ 37.466457 759.623622 0 ] (.:realisation) [ 200 0 R /XYZ 37.466457 471.845622 0 ] (.:recuperation-des-images) [ 218 0 R /XYZ 37.466457 240.604002 0 ] (.:recuperer-les-cookies) [ 237 0 R /XYZ 40.316457 419.812422 0 ] (.:resume) [ 192 0 R /XYZ 37.466457 685.328922 0 ] (.:resume-des-difficultes-techniques) [ 414 0 R /XYZ 37.466457 571.424922 0 ] (.:simuler-un-navigateur) [ 227 0 R /XYZ 40.316457 451.595622 0 ] (.:stockage-des-donnees) [ 414 0 R /XYZ 37.466457 740.609622 0 ] (.:taches) [ 214 0 R /XYZ 37.466457 771.023622 0 ] (.:tests) [ 414 0 R /XYZ 37.466457 645.491622 0 ] (.:texte) [ 297 0 R /XYZ 40.316457 771.023622 0 ] (.:tt) [ 218 0 R /XYZ 40.316457 771.023622 0 ] (.:tt1-tests-ocr-2) [ 218 0 R /XYZ 40.316457 718.310022 0 ] (.:tt2-tests-finaux-2) [ 218 0 R /XYZ 40.316457 578.153862 0 ] (CahierDesCharges/:) [ 416 0 R /XYZ 37.466457 771.023622 0 ] (CahierDesCharges/:cahier-des-charges) [ 416 0 R /XYZ 37.466457 759.623622 0 ] (CahierDesCharges/:cas-dutilisation) [ 430 0 R /XYZ 37.466457 138.793266 0 ] (CahierDesCharges/:contexte) [ 416 0 R /XYZ 37.466457 686.805222 0 ] (CahierDesCharges/:difficultes-techniques) [ 435 0 R /XYZ 37.466457 475.672422 0 ] (CahierDesCharges/:projet) [ 423 0 R /XYZ 37.466457 592.727622 0 ] (CahierDesCharges/:realisation) [ 423 0 R /XYZ 37.466457 286.876122 0 ] (Code/ConfigurationTool/:) [ 867 0 R /XYZ 37.466457 718.588422 0 ] (Code/ConfigurationTool/:configurationtoolcs) [ 867 0 R /XYZ 37.466457 718.588422 0 ] (Code/DriverData/:) [ 895 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverData/:driverdatacs) [ 895 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverDrsWindow/:) [ 917 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverDrsWindow/:driverdrswindowcs) [ 917 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverGapToLeaderWindow/:) [ 873 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverGapToLeaderWindow/:drivergaptoleaderwindowcs) [ 873 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverLapTimeWindow/:) [ 899 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverLapTimeWindow/:driverlaptimewindowcs) [ 899 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverNameWindow/:) [ 921 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverNameWindow/:drivernamewindowcs) [ 921 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverPositionWindow/:) [ 875 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverPositionWindow/:driverpositionwindowcs) [ 875 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverSectorWindow/:) [ 901 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverSectorWindow/:driversectorwindowcs) [ 901 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverTyresWindow/:) [ 923 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverTyresWindow/:drivertyreswindowcs) [ 923 0 R /XYZ 37.466457 751.643622 0 ] (Code/F1TVEmulator/:) [ 877 0 R /XYZ 37.466457 751.643622 0 ] (Code/F1TVEmulator/:f1tvemulatorcs) [ 877 0 R /XYZ 37.466457 751.643622 0 ] (Code/Form1/:) [ 903 0 R /XYZ 37.466457 751.643622 0 ] (Code/Form1/:form1cs) [ 903 0 R /XYZ 37.466457 751.643622 0 ] (Code/OcrImage/:) [ 927 0 R /XYZ 37.466457 751.643622 0 ] (Code/OcrImage/:ocrimagecs) [ 927 0 R /XYZ 37.466457 751.643622 0 ] (Code/Program/:) [ 885 0 R /XYZ 37.466457 751.643622 0 ] (Code/Program/:programcs) [ 885 0 R /XYZ 37.466457 751.643622 0 ] (Code/Reader/:) [ 905 0 R /XYZ 37.466457 751.643622 0 ] (Code/Reader/:readercs) [ 905 0 R /XYZ 37.466457 751.643622 0 ] (Code/Settings/:) [ 939 0 R /XYZ 37.466457 751.643622 0 ] (Code/Settings/:settingscs) [ 939 0 R /XYZ 37.466457 751.643622 0 ] (Code/Window/:) [ 887 0 R /XYZ 37.466457 751.643622 0 ] (Code/Window/:windowcs) [ 887 0 R /XYZ 37.466457 751.643622 0 ] (Code/Zone/:) [ 911 0 R /XYZ 37.466457 751.643622 0 ] (Code/Zone/:zonecs) [ 911 0 R /XYZ 37.466457 751.643622 0 ] (Code/recoverCookiesCSV/:) [ 949 0 R /XYZ 37.466457 751.643622 0 ] (Code/recoverCookiesCSV/:recovercookiescsvpy) [ 949 0 R /XYZ 37.466457 751.643622 0 ] (INXWIZI/) [ 867 0 R /XYZ 37.466457 759.623622 0 ] (INXWIZI/:) [ 867 0 R /XYZ 37.466457 771.023622 0 ] (author) [ 6 0 R /XYZ 51.146457 106.251742 0 ] (copyright) [ 6 0 R /XYZ 51.146457 82.539742 0 ] (doc-cover) [ 6 0 R /XYZ 28.346457 771.023622 0 ] (doc-toc) [ 8 0 R /XYZ 37.466457 771.023622 0 ] (jdb/:) [ 437 0 R /XYZ 37.466457 771.023622 0 ] (jdb/:26-avril-2023) [ 763 0 R /XYZ 37.466457 559.394022 0 ] (jdb/:jeudi-27-avril-2023) [ 766 0 R /XYZ 37.466457 317.480782 0 ] (jdb/:jeudi-30-mars-2023) [ 439 0 R /XYZ 37.466457 330.643313 0 ] (jdb/:jeudi-6-avril) [ 631 0 R /XYZ 37.466457 250.585350 0 ] (jdb/:journal-de-bord) [ 437 0 R /XYZ 37.466457 759.623622 0 ] (jdb/:lundi-1-mai-2023) [ 780 0 R /XYZ 37.466457 137.264682 0 ] (jdb/:lundi-24-avril-2023) [ 745 0 R /XYZ 37.466457 386.895102 0 ] (jdb/:lundi-3-avril) [ 521 0 R /XYZ 37.466457 771.023622 0 ] (jdb/:lundi-8-mai-2023) [ 811 0 R /XYZ 37.466457 686.526822 0 ] (jdb/:mardi-2-mai-2023) [ 784 0 R /XYZ 37.466457 186.104214 0 ] (jdb/:mardi-25-avril-2023) [ 753 0 R /XYZ 37.466457 205.106022 0 ] (jdb/:mardi-4-avril) [ 586 0 R /XYZ 37.466457 694.278822 0 ] (jdb/:mardi-9-mai-2023) [ 845 0 R /XYZ 37.466457 226.356188 0 ] (jdb/:mercredi-29-mars-2023) [ 437 0 R /XYZ 37.466457 718.588422 0 ] (jdb/:mercredi-5-avril) [ 606 0 R /XYZ 37.466457 125.723886 0 ] (jdb/:recrutement-payerne-mai-2023) [ 806 0 R /XYZ 37.466457 653.968422 0 ] (jdb/:vacances) [ 697 0 R /XYZ 37.466457 389.906022 0 ] (jdb/:vendredi-28-avril-2023) [ 780 0 R /XYZ 37.466457 771.023622 0 ] (jdb/:vendredi-31032023) [ 450 0 R /XYZ 37.466457 458.618022 0 ] (jdb/:vendredi-5-mai-2023) [ 806 0 R /XYZ 37.466457 571.919322 0 ] (jdb/:vendredi-6-avril-2023) [ 672 0 R /XYZ 37.466457 415.188330 0 ] ] +/Names [ (.:) [ 192 0 R /XYZ 37.466457 771.023622 0 ] (.:abstract) [ 198 0 R /XYZ 37.466457 131.212566 0 ] (.:affichage-des-donnees) [ 420 0 R /XYZ 37.466457 710.195622 0 ] (.:ameliorations-futures) [ 420 0 R /XYZ 37.466457 336.821622 0 ] (.:analyse-fonctionnelle) [ 224 0 R /XYZ 37.466457 384.861402 0 ] (.:analyse-organique) [ 224 0 R /XYZ 37.466457 310.794702 0 ] (.:cahier-des-charges) [ 204 0 R /XYZ 37.466457 127.426422 0 ] (.:calibration) [ 243 0 R /XYZ 40.316457 337.595622 0 ] (.:cas-dutilisation) [ 213 0 R /XYZ 37.466457 142.669266 0 ] (.:chiffres) [ 318 0 R /XYZ 40.316457 384.462822 0 ] (.:comment-faire) [ 228 0 R /XYZ 40.316457 208.515666 0 ] (.:conclusion) [ 420 0 R /XYZ 37.466457 262.754922 0 ] (.:controler-le-navigateur) [ 238 0 R /XYZ 40.316457 538.688022 0 ] (.:description-du-besoin) [ 204 0 R /XYZ 37.466457 522.959622 0 ] (.:differences-sur-le-cahier-des-charges) [ 218 0 R /XYZ 37.466457 293.549622 0 ] (.:difficultes-techniques) [ 218 0 R /XYZ 37.466457 479.548422 0 ] (.:drs) [ 401 0 R /XYZ 40.316457 322.168422 0 ] (.:dt) [ 220 0 R /XYZ 40.316457 493.384662 0 ] (.:dt1-creation-du-poster-1) [ 220 0 R /XYZ 40.316457 453.074262 0 ] (.:dt2-documentation-analyse-de-lexistant-2) [ 220 0 R /XYZ 40.316457 370.282902 0 ] (.:dt3-documentation-analyse-organique-5) [ 220 0 R /XYZ 40.316457 299.894742 0 ] (.:dt4-documentation-analyse-fonctionnelle-2) [ 220 0 R /XYZ 40.316457 196.948182 0 ] (.:dt5-documentation-tests-1) [ 222 0 R /XYZ 40.316457 771.023622 0 ] (.:dt6-documentation-reste-2) [ 222 0 R /XYZ 40.316457 720.790662 0 ] (.:ethique-du-projet) [ 420 0 R /XYZ 37.466457 423.291522 0 ] (.:filtres-et-methodes-sur-les-images) [ 401 0 R /XYZ 40.316457 284.338662 0 ] (.:filtres-et-traitement) [ 282 0 R /XYZ 40.316457 465.594822 0 ] (.:fonctionnement-general) [ 243 0 R /XYZ 40.316457 131.940822 0 ] (.:interpretation-des-donnees) [ 420 0 R /XYZ 37.466457 771.023622 0 ] (.:introduction) [ 198 0 R /XYZ 37.466457 731.488422 0 ] (.:ocr) [ 243 0 R /XYZ 37.466457 289.533222 0 ] (.:optimisation-du-programme) [ 420 0 R /XYZ 37.466457 497.358222 0 ] (.:planning-effectif-et-differences) [ 224 0 R /XYZ 37.466457 458.928102 0 ] (.:planning-previsionnel) [ 218 0 R /XYZ 37.466457 219.482922 0 ] (.:pneus) [ 337 0 R /XYZ 40.316457 221.934822 0 ] (.:predictions) [ 420 0 R /XYZ 37.466457 679.781622 0 ] (.:projet) [ 206 0 R /XYZ 37.466457 718.310022 0 ] (.:pt) [ 220 0 R /XYZ 40.316457 625.834422 0 ] (.:pt1-preparation-au-travail-de-diplome-2) [ 220 0 R /XYZ 40.316457 585.524022 0 ] (.:pt1-programmation-recuperation-des-images-3) [ 222 0 R /XYZ 40.316457 608.496102 0 ] (.:pt2-programmation-ocr-5) [ 222 0 R /XYZ 40.316457 455.936742 0 ] (.:pt3-programmation-stockage-et-modele-5) [ 222 0 R /XYZ 40.316457 360.742182 0 ] (.:pt4-programmation-vue-de-lapp-5) [ 222 0 R /XYZ 40.316457 277.950822 0 ] (.:pt5-programmation-mise-en-commun-3) [ 222 0 R /XYZ 40.316457 182.756262 0 ] (.:pt_1) [ 222 0 R /XYZ 40.316457 648.806502 0 ] (.:rapport-track-trends-v10) [ 192 0 R /XYZ 37.466457 759.623622 0 ] (.:realisation) [ 206 0 R /XYZ 37.466457 419.132022 0 ] (.:recuperation-des-images) [ 224 0 R /XYZ 37.466457 240.604002 0 ] (.:recuperer-les-cookies) [ 243 0 R /XYZ 40.316457 419.812422 0 ] (.:resume) [ 198 0 R /XYZ 37.466457 685.328922 0 ] (.:resume-des-difficultes-techniques) [ 420 0 R /XYZ 37.466457 571.424922 0 ] (.:simuler-un-navigateur) [ 233 0 R /XYZ 40.316457 451.595622 0 ] (.:stockage-des-donnees) [ 420 0 R /XYZ 37.466457 740.609622 0 ] (.:taches) [ 220 0 R /XYZ 37.466457 771.023622 0 ] (.:tests) [ 420 0 R /XYZ 37.466457 645.491622 0 ] (.:texte) [ 303 0 R /XYZ 40.316457 771.023622 0 ] (.:tt) [ 224 0 R /XYZ 40.316457 771.023622 0 ] (.:tt1-tests-ocr-2) [ 224 0 R /XYZ 40.316457 718.310022 0 ] (.:tt2-tests-finaux-2) [ 224 0 R /XYZ 40.316457 578.153862 0 ] (CahierDesCharges/:) [ 422 0 R /XYZ 37.466457 771.023622 0 ] (CahierDesCharges/:cahier-des-charges) [ 422 0 R /XYZ 37.466457 759.623622 0 ] (CahierDesCharges/:cas-dutilisation) [ 436 0 R /XYZ 37.466457 138.793266 0 ] (CahierDesCharges/:contexte) [ 422 0 R /XYZ 37.466457 686.805222 0 ] (CahierDesCharges/:difficultes-techniques) [ 441 0 R /XYZ 37.466457 475.672422 0 ] (CahierDesCharges/:projet) [ 429 0 R /XYZ 37.466457 592.727622 0 ] (CahierDesCharges/:realisation) [ 429 0 R /XYZ 37.466457 286.876122 0 ] (Code/ConfigurationTool/:) [ 873 0 R /XYZ 37.466457 718.588422 0 ] (Code/ConfigurationTool/:configurationtoolcs) [ 873 0 R /XYZ 37.466457 718.588422 0 ] (Code/DriverData/:) [ 901 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverData/:driverdatacs) [ 901 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverDrsWindow/:) [ 923 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverDrsWindow/:driverdrswindowcs) [ 923 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverGapToLeaderWindow/:) [ 879 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverGapToLeaderWindow/:drivergaptoleaderwindowcs) [ 879 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverLapTimeWindow/:) [ 905 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverLapTimeWindow/:driverlaptimewindowcs) [ 905 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverNameWindow/:) [ 927 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverNameWindow/:drivernamewindowcs) [ 927 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverPositionWindow/:) [ 881 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverPositionWindow/:driverpositionwindowcs) [ 881 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverSectorWindow/:) [ 907 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverSectorWindow/:driversectorwindowcs) [ 907 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverTyresWindow/:) [ 929 0 R /XYZ 37.466457 751.643622 0 ] (Code/DriverTyresWindow/:drivertyreswindowcs) [ 929 0 R /XYZ 37.466457 751.643622 0 ] (Code/F1TVEmulator/:) [ 883 0 R /XYZ 37.466457 751.643622 0 ] (Code/F1TVEmulator/:f1tvemulatorcs) [ 883 0 R /XYZ 37.466457 751.643622 0 ] (Code/Form1/:) [ 909 0 R /XYZ 37.466457 751.643622 0 ] (Code/Form1/:form1cs) [ 909 0 R /XYZ 37.466457 751.643622 0 ] (Code/OcrImage/:) [ 933 0 R /XYZ 37.466457 751.643622 0 ] (Code/OcrImage/:ocrimagecs) [ 933 0 R /XYZ 37.466457 751.643622 0 ] (Code/Program/:) [ 891 0 R /XYZ 37.466457 751.643622 0 ] (Code/Program/:programcs) [ 891 0 R /XYZ 37.466457 751.643622 0 ] (Code/Reader/:) [ 911 0 R /XYZ 37.466457 751.643622 0 ] (Code/Reader/:readercs) [ 911 0 R /XYZ 37.466457 751.643622 0 ] (Code/Settings/:) [ 945 0 R /XYZ 37.466457 751.643622 0 ] (Code/Settings/:settingscs) [ 945 0 R /XYZ 37.466457 751.643622 0 ] (Code/Window/:) [ 893 0 R /XYZ 37.466457 751.643622 0 ] (Code/Window/:windowcs) [ 893 0 R /XYZ 37.466457 751.643622 0 ] (Code/Zone/:) [ 917 0 R /XYZ 37.466457 751.643622 0 ] (Code/Zone/:zonecs) [ 917 0 R /XYZ 37.466457 751.643622 0 ] (Code/recoverCookiesCSV/:) [ 955 0 R /XYZ 37.466457 751.643622 0 ] (Code/recoverCookiesCSV/:recovercookiescsvpy) [ 955 0 R /XYZ 37.466457 751.643622 0 ] (INXWIZI/) [ 873 0 R /XYZ 37.466457 759.623622 0 ] (INXWIZI/:) [ 873 0 R /XYZ 37.466457 771.023622 0 ] (author) [ 6 0 R /XYZ 51.146457 106.251742 0 ] (copyright) [ 6 0 R /XYZ 51.146457 82.539742 0 ] (doc-cover) [ 6 0 R /XYZ 28.346457 771.023622 0 ] (doc-toc) [ 8 0 R /XYZ 37.466457 771.023622 0 ] (jdb/:) [ 443 0 R /XYZ 37.466457 771.023622 0 ] (jdb/:26-avril-2023) [ 769 0 R /XYZ 37.466457 559.394022 0 ] (jdb/:jeudi-27-avril-2023) [ 772 0 R /XYZ 37.466457 317.480782 0 ] (jdb/:jeudi-30-mars-2023) [ 445 0 R /XYZ 37.466457 330.643313 0 ] (jdb/:jeudi-6-avril) [ 637 0 R /XYZ 37.466457 250.585350 0 ] (jdb/:journal-de-bord) [ 443 0 R /XYZ 37.466457 759.623622 0 ] (jdb/:lundi-1-mai-2023) [ 786 0 R /XYZ 37.466457 137.264682 0 ] (jdb/:lundi-24-avril-2023) [ 751 0 R /XYZ 37.466457 386.895102 0 ] (jdb/:lundi-3-avril) [ 527 0 R /XYZ 37.466457 771.023622 0 ] (jdb/:lundi-8-mai-2023) [ 817 0 R /XYZ 37.466457 686.526822 0 ] (jdb/:mardi-2-mai-2023) [ 790 0 R /XYZ 37.466457 186.104214 0 ] (jdb/:mardi-25-avril-2023) [ 759 0 R /XYZ 37.466457 205.106022 0 ] (jdb/:mardi-4-avril) [ 592 0 R /XYZ 37.466457 694.278822 0 ] (jdb/:mardi-9-mai-2023) [ 851 0 R /XYZ 37.466457 226.356188 0 ] (jdb/:mercredi-29-mars-2023) [ 443 0 R /XYZ 37.466457 718.588422 0 ] (jdb/:mercredi-5-avril) [ 612 0 R /XYZ 37.466457 125.723886 0 ] (jdb/:recrutement-payerne-mai-2023) [ 812 0 R /XYZ 37.466457 653.968422 0 ] (jdb/:vacances) [ 703 0 R /XYZ 37.466457 389.906022 0 ] (jdb/:vendredi-28-avril-2023) [ 786 0 R /XYZ 37.466457 771.023622 0 ] (jdb/:vendredi-31032023) [ 456 0 R /XYZ 37.466457 458.618022 0 ] (jdb/:vendredi-5-mai-2023) [ 812 0 R /XYZ 37.466457 571.919322 0 ] (jdb/:vendredi-6-avril-2023) [ 678 0 R /XYZ 37.466457 415.188330 0 ] ] >> >> >> @@ -41,152 +41,152 @@ endobj >> >> /XObject << -/i83e8d47bdf7867037eaedcf9b3ba91c5 1085 0 R -/i3b497ccb2cc72102529af416dabfdc79 1087 0 R -/i6d29f3061def42a6d984946e7bf957bb 1089 0 R -/i862acd95fd3b7c98ae07759a85e97bb4 1091 0 R -/ieb9f461ea42dd450637413a739caf3e2 1093 0 R -/i95354e0ced090a84ad5029bc54cfca25 1094 0 R -/i483b48f68c365836d5538b7b2428e26c 1095 0 R -/idfec847ea3aa6025b1e2952efcd456c5 1097 0 R -/i4e5041030b7abb6f99d9953487f6523d 1099 0 R -/ibbad9b91af3faa229b8648d050a4979f 1101 0 R -/i864ecd1f04e13a5e2e05b34e05efb5e8 1103 0 R -/idf8fc4e68ed1a33ad823ad667a0f68b2 1105 0 R -/id64e468e5c9063255b0cbcd967af36fb 1107 0 R -/ifeb68a942f3d59c6d6c0492b04369c72 1109 0 R -/i456fc3b8c9506626329986ad2a6165cb 1111 0 R -/ifd045f27144497870f6754b815faf42d 1113 0 R -/id9ea2680132a7ada299a6ccd6eb4721d 1115 0 R -/idbbed2f6cd12f34d2e6d79fc9264704e 1117 0 R -/i5753d0cb39836c974aeb4d3e181c73f9 1119 0 R -/i4df9917b14b40c6938ccbe418deb9311 1121 0 R -/i56ebb8aaf4dabde9b21e2f3c398d3d2a 1123 0 R -/ieabc84d1373def37e713cb81d92ead72 1125 0 R -/i1864b403764fce5ca0f52759b1d746da 1127 0 R -/i0b08055253198904c9e5e3d17751a27b 1129 0 R -/id05b28f6c40c771eaa05a88211e94946 1131 0 R -/i9709e3d755f4e3e4b72cba138dd67a33 1133 0 R -/i9ce7d04a1a468903e102e54b467a8794 1135 0 R -/i5c81c682a716b2574c0d25abb24f2299 1137 0 R -/i415d130bf16a4f457995458e1fd54a24 1139 0 R -/i980b0c2608fcb02021172158435b33d0 1141 0 R -/ide208052fad45aa250cfb9ff02c56a24 1143 0 R -/i4578e049f08c54f0b208221322d4af40 1145 0 R -/i1490d2d06018026d86032b98446daac4 1147 0 R -/i118a90245cba588084cf533bb078c9ba 1149 0 R -/i46fb1d6ec7ef17435e8c70cddf76e1e8 1151 0 R -/i697f03ebf6060d61aee540142fbc9866 1153 0 R -/i592319d66ce72d58b8fbcd4fcd595090 1155 0 R -/ib6da13f7cbd6ea9cfb728bbe653974fe 1157 0 R -/ie88d0360fc428391dccbfb474e640d0f 1159 0 R -/i28bfe17ffb6c7dc803ab5f0c8a765947 1161 0 R -/i537e253324baa41ff1b737c064429ade 1163 0 R -/i2a382b5b92ab4a00fce5007637b78832 1165 0 R -/i91be234c88e16692309a3cd72c739d08 1167 0 R -/i67da3ed2201c45af09c71bdb7ba05f4d 1169 0 R -/i269c5dad91493a1dc1d485a1196a8250 1171 0 R -/ie434869461a294629a802947e9cde65e 1173 0 R -/i48c3a07e2c82d5cbd8423c214a04b083 1175 0 R -/ibc8c79593eebf4502cc03c8ed510cc7e 1177 0 R -/i8aca4212570ed49b69bf0d61cbecb6d4 1179 0 R -/i01572a2fb14977d7bba1a9a5cc853862 1181 0 R -/i20ce8e4546d289f37dc98ac5683994c0 1183 0 R -/icb9a987068776512d1cd8c31d9940aef 1184 0 R -/i9632cb825142a03f242e3843f07417a3 1185 0 R -/ic04167c903cb799760037987b6953254 1186 0 R -/i1728bb6fe8b5e256820f39a8fccf896f 1187 0 R -/i274c74455d786244597d823a9a5a533f 1188 0 R -/i69a3d2e2aa39a9aa89691865bfe087b7 1190 0 R -/i16e63abf761e5e49028def0764d5fc01 1192 0 R -/i28e63526d9b0dab84592ba43edec1bbc 1193 0 R -/idf92a68daa3cb2352d343132c5327258 1194 0 R -/ie47e3ccc7158c83f627c5fdbacca0b01 1196 0 R -/i016f5a1345314db3ad10eae1790b4f1c 1198 0 R -/ic6f361c3184e5ab67e17e3506d5d80d8 1200 0 R -/ia64748b5466fee915378fccf5c7a1b47 1202 0 R -/i1f75ecd3cdea65c723059e748dfb4d86 1204 0 R -/i93aa95fe267034962c6a8cf1c6e1a97c 1206 0 R -/i38d424149caf0ede6ac6f1785736db7b 1208 0 R -/if977c8a66a7b56abf4971466d44c94ad 1210 0 R -/i271db94350f76791c3ea696f9e1e2071 1212 0 R -/ib36bfe1d2070f0d8e1278878549c85a9 1214 0 R -/i0cdd3aa0068844a5910435ea46c7ebab 1216 0 R -/ib1f594f771c011faa047b1fc33398489 1218 0 R -/i29c1e0a342594acb8a37494fb09aaeac 1220 0 R -/i49a427ed1e41991b547dd37f3a5a968a 1222 0 R -/ib6c959bc47025eb9e97c7ffe143b83e0 1224 0 R -/i5c1fba789549d426899a38b1df2bf165 1226 0 R -/i1b87f5817ee048a6e58ae68c7c648590 1228 0 R -/i144d9afaad5db5ba25a482aa868b5309 1230 0 R -/ie2bc4160dd648fa132f79285a13ddbf6 1232 0 R -/i49558b02479b930cc9f0044ee5ae5854 1234 0 R -/i1466d35bb8bb81058e39171cf3f175e7 1236 0 R -/ib770143957879744018ff6c0742d739f 1238 0 R -/i44b99004acfb76f0f80ced0851d1fa22 1240 0 R -/ie98cebf629d52cedf854f75ccd8a3948 1242 0 R -/iebd158e6cffc746d0fea2fe798c40b0a 1244 0 R -/i3380ccf226f8f9ac934d4b4bbd547ebb 1246 0 R -/ie828749a19dddf3ea0dba96ddcaa47fc 1248 0 R -/i59d462a4ebc54ddc0dc1d694d8b152cf 1250 0 R -/i1ffc17e32362c2641f0d85f333cc69ed 1252 0 R -/ic2456a56e464f0d1acb55f84beaee6d3 1254 0 R -/i28fa2200dc4e7542a868e4a9da99aca9 1256 0 R -/i8a3b58d053c54439cdc19422af794da3 1258 0 R -/i17b8b9937f6e4599ff6db9f73f823096 1260 0 R -/i7c71fef6e7f50ada1dc3021f9bac6bba 1262 0 R -/i65221d140fd8ba236765a749aa48f9b6 1264 0 R -/i127689757da4e743ab7a9b9b9e0ccf8f 1266 0 R -/i21577e8bc1d8d0af76417ee7724edb84 1268 0 R -/ie38f9dbb3359324964f4e9171fb664b1 1270 0 R -/i5ff01cda45d74112993c7d72fb873f01 1272 0 R -/ibb0d13191f889a5aeb4134aa1b6935d2 1274 0 R -/i3accb128fcf1262e5b20f23a21d5bc61 1276 0 R -/icfed19ccda34e8dc2e3905610222faa9 1278 0 R -/i85c62db712f20b31b81ad2e6413309da 1280 0 R -/ic2a9a88bbf715284567f72f381668df2 1282 0 R -/ib9f9f07787c80c06641932388c3bdb74 1284 0 R -/i8a6f2d2f5a28c0af2c38024ae0081658 1286 0 R -/i225d8b520d32dd86767c2dcc61d0fba4 1288 0 R -/i4bffedf109758ad04d67bde2f542c422 1290 0 R -/ia54d20fa729a8068718b8c7d510ad027 1292 0 R -/iad2faaa5ca488113e9074ac5402d7efe 1294 0 R -/ifd92504eb5e8bf7df66b70cf4ce57dd5 1296 0 R -/id109e171ec05f9ad15797b8d83229356 1298 0 R -/iab5e53595722b1f5258144979d9f2ac3 1300 0 R -/i05f15b48a6e23194831c567df021af3f 1301 0 R -/i6c02b2a0d8bd39c599dd34ae7158267c 1303 0 R -/ib904c5ff40856935e447b3ddd76306c8 1305 0 R -/i69c3f44e5bd21a489ea8fcf335f7c44f 1307 0 R -/i2718fd348fdb22b641539ca636a9ed06 1309 0 R -/ief51d1e924220581210ee50401220d6b 1311 0 R -/i2e64122b51e48609e16d295e21626056 1313 0 R -/i3de64d04d5d6ec0507edeb9881576677 1315 0 R -/i0c483fc6885782fea1d33432172fcc18 1317 0 R -/if6b2541f2fea69bec681b5326e2cb236 1319 0 R -/i8646a456c3049dc41308c7715ce69f8b 1321 0 R -/i9a58a9c98de07135b36b09a7194fa17d 1323 0 R -/if49ae9e180658fe14ee6ba818d9aa65a 1325 0 R -/i808299d0a4329db339a1104fac2fb606 1327 0 R -/i7a6f9b89d58c4fec179ba918c226e361 1329 0 R -/i9df4cb3e557d5ca5fa480b1257d28838 1331 0 R -/i307658129ace4da16ce8e4d3bc2182c4 1333 0 R -/i5bc9aa345ebaa493ffa10f273970179e 1335 0 R -/i8705a48d8dbb4086afc34aaf93e995b9 1337 0 R -/i95fc521c77451596e6cea9e4878808f3 1339 0 R -/ic63e75e4660311e5e6282859b1220ffd 1341 0 R -/i3cb63d24a6d374f2471e18ca1df497c5 1343 0 R -/i65752d24f4a428052ecdb19d1f8d0c19 1345 0 R -/iac56279099050bf3f6660dc50d5fae15 1347 0 R -/i57f5883427bd603b0769e87f2691de4b 1349 0 R -/i93e09802d69c40bc9e388d83c59814c0 1351 0 R -/i7ad22ed2e653a4178ce918640f691141 1353 0 R +/i83e8d47bdf7867037eaedcf9b3ba91c5 1093 0 R +/i3b497ccb2cc72102529af416dabfdc79 1095 0 R +/i6d29f3061def42a6d984946e7bf957bb 1097 0 R +/i862acd95fd3b7c98ae07759a85e97bb4 1099 0 R +/ieb9f461ea42dd450637413a739caf3e2 1101 0 R +/i95354e0ced090a84ad5029bc54cfca25 1102 0 R +/i483b48f68c365836d5538b7b2428e26c 1103 0 R +/idfec847ea3aa6025b1e2952efcd456c5 1105 0 R +/i4e5041030b7abb6f99d9953487f6523d 1107 0 R +/ibbad9b91af3faa229b8648d050a4979f 1109 0 R +/i864ecd1f04e13a5e2e05b34e05efb5e8 1111 0 R +/idf8fc4e68ed1a33ad823ad667a0f68b2 1113 0 R +/id64e468e5c9063255b0cbcd967af36fb 1115 0 R +/ifeb68a942f3d59c6d6c0492b04369c72 1117 0 R +/i456fc3b8c9506626329986ad2a6165cb 1119 0 R +/ifd045f27144497870f6754b815faf42d 1121 0 R +/id9ea2680132a7ada299a6ccd6eb4721d 1123 0 R +/idbbed2f6cd12f34d2e6d79fc9264704e 1125 0 R +/i5753d0cb39836c974aeb4d3e181c73f9 1127 0 R +/i4df9917b14b40c6938ccbe418deb9311 1129 0 R +/i56ebb8aaf4dabde9b21e2f3c398d3d2a 1131 0 R +/ieabc84d1373def37e713cb81d92ead72 1133 0 R +/i1864b403764fce5ca0f52759b1d746da 1135 0 R +/i0b08055253198904c9e5e3d17751a27b 1137 0 R +/id05b28f6c40c771eaa05a88211e94946 1139 0 R +/i9709e3d755f4e3e4b72cba138dd67a33 1141 0 R +/i9ce7d04a1a468903e102e54b467a8794 1143 0 R +/i5c81c682a716b2574c0d25abb24f2299 1145 0 R +/i415d130bf16a4f457995458e1fd54a24 1147 0 R +/i980b0c2608fcb02021172158435b33d0 1149 0 R +/ide208052fad45aa250cfb9ff02c56a24 1151 0 R +/i4578e049f08c54f0b208221322d4af40 1153 0 R +/i1490d2d06018026d86032b98446daac4 1155 0 R +/i118a90245cba588084cf533bb078c9ba 1157 0 R +/i46fb1d6ec7ef17435e8c70cddf76e1e8 1159 0 R +/i697f03ebf6060d61aee540142fbc9866 1161 0 R +/i592319d66ce72d58b8fbcd4fcd595090 1163 0 R +/ib6da13f7cbd6ea9cfb728bbe653974fe 1165 0 R +/ie88d0360fc428391dccbfb474e640d0f 1167 0 R +/i28bfe17ffb6c7dc803ab5f0c8a765947 1169 0 R +/i537e253324baa41ff1b737c064429ade 1171 0 R +/i2a382b5b92ab4a00fce5007637b78832 1173 0 R +/i91be234c88e16692309a3cd72c739d08 1175 0 R +/i67da3ed2201c45af09c71bdb7ba05f4d 1177 0 R +/i269c5dad91493a1dc1d485a1196a8250 1179 0 R +/ie434869461a294629a802947e9cde65e 1181 0 R +/i48c3a07e2c82d5cbd8423c214a04b083 1183 0 R +/ibc8c79593eebf4502cc03c8ed510cc7e 1185 0 R +/i8aca4212570ed49b69bf0d61cbecb6d4 1187 0 R +/i01572a2fb14977d7bba1a9a5cc853862 1189 0 R +/i20ce8e4546d289f37dc98ac5683994c0 1191 0 R +/icb9a987068776512d1cd8c31d9940aef 1192 0 R +/i9632cb825142a03f242e3843f07417a3 1193 0 R +/ic04167c903cb799760037987b6953254 1194 0 R +/i1728bb6fe8b5e256820f39a8fccf896f 1195 0 R +/i274c74455d786244597d823a9a5a533f 1196 0 R +/i69a3d2e2aa39a9aa89691865bfe087b7 1198 0 R +/i16e63abf761e5e49028def0764d5fc01 1200 0 R +/i28e63526d9b0dab84592ba43edec1bbc 1201 0 R +/idf92a68daa3cb2352d343132c5327258 1202 0 R +/ie47e3ccc7158c83f627c5fdbacca0b01 1204 0 R +/i016f5a1345314db3ad10eae1790b4f1c 1206 0 R +/ic6f361c3184e5ab67e17e3506d5d80d8 1208 0 R +/ia64748b5466fee915378fccf5c7a1b47 1210 0 R +/i1f75ecd3cdea65c723059e748dfb4d86 1212 0 R +/i93aa95fe267034962c6a8cf1c6e1a97c 1214 0 R +/i38d424149caf0ede6ac6f1785736db7b 1216 0 R +/if977c8a66a7b56abf4971466d44c94ad 1218 0 R +/i271db94350f76791c3ea696f9e1e2071 1220 0 R +/ib36bfe1d2070f0d8e1278878549c85a9 1222 0 R +/i0cdd3aa0068844a5910435ea46c7ebab 1224 0 R +/ib1f594f771c011faa047b1fc33398489 1226 0 R +/i29c1e0a342594acb8a37494fb09aaeac 1228 0 R +/i49a427ed1e41991b547dd37f3a5a968a 1230 0 R +/ib6c959bc47025eb9e97c7ffe143b83e0 1232 0 R +/i5c1fba789549d426899a38b1df2bf165 1234 0 R +/i1b87f5817ee048a6e58ae68c7c648590 1236 0 R +/i144d9afaad5db5ba25a482aa868b5309 1238 0 R +/ie2bc4160dd648fa132f79285a13ddbf6 1240 0 R +/i49558b02479b930cc9f0044ee5ae5854 1242 0 R +/i1466d35bb8bb81058e39171cf3f175e7 1244 0 R +/ib770143957879744018ff6c0742d739f 1246 0 R +/i44b99004acfb76f0f80ced0851d1fa22 1248 0 R +/ie98cebf629d52cedf854f75ccd8a3948 1250 0 R +/iebd158e6cffc746d0fea2fe798c40b0a 1252 0 R +/i3380ccf226f8f9ac934d4b4bbd547ebb 1254 0 R +/ie828749a19dddf3ea0dba96ddcaa47fc 1256 0 R +/i59d462a4ebc54ddc0dc1d694d8b152cf 1258 0 R +/i1ffc17e32362c2641f0d85f333cc69ed 1260 0 R +/ic2456a56e464f0d1acb55f84beaee6d3 1262 0 R +/i28fa2200dc4e7542a868e4a9da99aca9 1264 0 R +/i8a3b58d053c54439cdc19422af794da3 1266 0 R +/i17b8b9937f6e4599ff6db9f73f823096 1268 0 R +/i7c71fef6e7f50ada1dc3021f9bac6bba 1270 0 R +/i65221d140fd8ba236765a749aa48f9b6 1272 0 R +/i127689757da4e743ab7a9b9b9e0ccf8f 1274 0 R +/i21577e8bc1d8d0af76417ee7724edb84 1276 0 R +/ie38f9dbb3359324964f4e9171fb664b1 1278 0 R +/i5ff01cda45d74112993c7d72fb873f01 1280 0 R +/ibb0d13191f889a5aeb4134aa1b6935d2 1282 0 R +/i3accb128fcf1262e5b20f23a21d5bc61 1284 0 R +/icfed19ccda34e8dc2e3905610222faa9 1286 0 R +/i85c62db712f20b31b81ad2e6413309da 1288 0 R +/ic2a9a88bbf715284567f72f381668df2 1290 0 R +/ib9f9f07787c80c06641932388c3bdb74 1292 0 R +/i8a6f2d2f5a28c0af2c38024ae0081658 1294 0 R +/i225d8b520d32dd86767c2dcc61d0fba4 1296 0 R +/i4bffedf109758ad04d67bde2f542c422 1298 0 R +/ia54d20fa729a8068718b8c7d510ad027 1300 0 R +/iad2faaa5ca488113e9074ac5402d7efe 1302 0 R +/ifd92504eb5e8bf7df66b70cf4ce57dd5 1304 0 R +/id109e171ec05f9ad15797b8d83229356 1306 0 R +/iab5e53595722b1f5258144979d9f2ac3 1308 0 R +/i05f15b48a6e23194831c567df021af3f 1309 0 R +/i6c02b2a0d8bd39c599dd34ae7158267c 1311 0 R +/ib904c5ff40856935e447b3ddd76306c8 1313 0 R +/i69c3f44e5bd21a489ea8fcf335f7c44f 1315 0 R +/i2718fd348fdb22b641539ca636a9ed06 1317 0 R +/ief51d1e924220581210ee50401220d6b 1319 0 R +/i2e64122b51e48609e16d295e21626056 1321 0 R +/i3de64d04d5d6ec0507edeb9881576677 1323 0 R +/i0c483fc6885782fea1d33432172fcc18 1325 0 R +/if6b2541f2fea69bec681b5326e2cb236 1327 0 R +/i8646a456c3049dc41308c7715ce69f8b 1329 0 R +/i9a58a9c98de07135b36b09a7194fa17d 1331 0 R +/if49ae9e180658fe14ee6ba818d9aa65a 1333 0 R +/i808299d0a4329db339a1104fac2fb606 1335 0 R +/i7a6f9b89d58c4fec179ba918c226e361 1337 0 R +/i9df4cb3e557d5ca5fa480b1257d28838 1339 0 R +/i307658129ace4da16ce8e4d3bc2182c4 1341 0 R +/i5bc9aa345ebaa493ffa10f273970179e 1343 0 R +/i8705a48d8dbb4086afc34aaf93e995b9 1345 0 R +/i95fc521c77451596e6cea9e4878808f3 1347 0 R +/ic63e75e4660311e5e6282859b1220ffd 1349 0 R +/i3cb63d24a6d374f2471e18ca1df497c5 1351 0 R +/i65752d24f4a428052ecdb19d1f8d0c19 1353 0 R +/iac56279099050bf3f6660dc50d5fae15 1355 0 R +/i57f5883427bd603b0769e87f2691de4b 1357 0 R +/i93e09802d69c40bc9e388d83c59814c0 1359 0 R +/i7ad22ed2e653a4178ce918640f691141 1361 0 R >> /Pattern << >> /Shading << >> -/Font 1084 0 R +/Font 1092 0 R >> endobj 5 0 obj @@ -215,322 +215,328 @@ endobj 7 0 obj << /Filter /FlateDecode -/Length 48705 +/Length 48796 >> stream -xMeɹ_y P 6` $,$4KMH?xNUz׿ٗʊ}Dȳ2_?vs˧~stoOt=z??ZOZ}:m{]??TzOu˲9s/˧/7[k~?ey1~ۿ?=_?<f _3a?m{yAf3^<`/x\o?m?E~w??u_?_^?^}ܖs|h?pG?el]?ۦcՏ?َ?/3~>m}xFO<ᛇ?|f<_~O:Ⱥ@"yr;\5:@jlp:vR/R{g]=5 .]==\;^_xjv pk ϗz[p ;fpWYpƄS%X^5ykƐ C^^w*2oe;uX̵ ,5u+,5u3gYk15m9ҵXk۱k֘GҵXkk՘Vcޟ3y1cL8t-:ҵk群kטJb1_%Sc4|gG^t-}Z5ӵ8j϶Qc~+&0ҵYoϔ^:ҵyoFk5O;]bLco_W5qkQ߾>k5x rkQ߾4}ҘkSEM1`j܏Lע&t-j1dj籧1<+\Zp-֚k&Sc'\.:µXkkIcyp^ws̓ZiuE̓6'WךsZ5ϓ֫k̓kMյ5?1sAkQJNյs̼Z<8zuypܕZqҘkOմ^]kڴ^]k^]k^]k\Z5ߴ^j/@i<80zuyp<3zuyp :WNխZzuy-zuy״^j<3WIխzuy|Ws̓֫[̓%3Wו֫[̓ײVi^Iս{̓cHս{<Әk^}ս{̓c֫{̓W3#WXM$3,quyY^սq+֫{̓WbOcy^սg{̓W=5>+W#5^k|θϕ֫R=cMZyQ֫Wb:@֘:VÜqu&eδ^=WzGuc1qjw_=}ט$Ƽטz5'/OW+]1WdzL_=<8nͷ%ỹ1q8kk_2q8k[_=Nc>k۝r\5=Wyeq՘z\5#wԘϸz53w]cԘz<8)W;5.w_=j\z<7*^><J{?x}im,+|=o%|Y*ӷ]ͯ?o"~#>64-{A?;9o׵/KqC>sdxRc#ZD%25Ao+zk{sc>ߟ %1Hu>5XW ra,m{X>a,ֻnv.j>|13c.cZzI^1̱ũ8x=ӧyuO?sNjPײw E̘Lc.Ҏ41YTQp3* 41:xcis00ǖ1ޯ@LཌMϘﱘJct?ncIཌྷ/41V˙^3X/=nx6q{{m̃Xhm̃eOۘ1m%oc #>oƋB<8Kz}<8netyyUPϘ<=6gH41>cM5sq<ϧgZ{qk1=dƘ1cypFϑ3g[tyَsK=O<^1>{m'3\{q<#y9nϘK+oc|miW -ڼOU` -*0^v*0=i -1Q -׸K{U`Ƅ7 Gڃ=wǯ㵏MYǘG=~=*0^GjcSʕƳ1Ǔx]5>W1t_k̸ -׽ -= i<S?/1su{*0ƂIU`,gKU`,v*0eܢc뺥*0qX^X`XƅM{U`,ciX\cG81s(c>+W\w6ǘO fz#1`ƘH{U`,cq_]K|}~O;Ìxs -u<s㟞t_:/>W>zO븙KU`cXLU`? -1 -Xt_Ƙ\Xniv<2LdƘ O2c$3|gڃcL:t_zc~+W1[X= -5^6x<8t_Ƹsr] -m*0KOszIU`l1EGǯc{ϘǓHU`[;>WU@3=~[ŁqW1t_;8TtU),:◯Ez." YVY>]طOY(!PBdH,Y(!PBdH,Y(!PBdH,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y!pCdL,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y!pCdL,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y!pCdaGn,Y!pCdEn,Y!pCdEn,Y!pCdEn,Y!pCdEn,Y!pCdEn,Y!pCd$pCd " 5-pCd " 5-pCd " 5-pCd " 3=pCd " 5-pCd " 5-pCd " 5-pCd " 5-pCd " 5-pCd " 5'" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Y鑅" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Y9,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y!pCdL,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y!pCdBM,Y,~#?z.׷ܳ}-*8sh᳽CϧR/˟,aE -G13=|6xC1f#=S}0 %J6li %J6li %J6l 7n6li 7n6li 7n6li 7n6li 7n6li 7n6li 7n6li 7n6li 7n6li 7n6l 7n6li 7n6li 7n6li 7n6li 7n6li 7n6li 7n6li 7n6li 7n6li 7n6l 7n6li 7n6li 7n6li 7n6li 7n6li 7n6li 7n6li 7n6li 7n6li 7n6`  7n6Դ`  7n6Դ`  7n6Դ`  7n6Դ`  7n6Դ`  7n6Ԝn6l!pCn6l!pCn6l!pCn6l!pCan6l!pCn6l!pCn6l!pCn6l!pCn6l!pCn6l!pCn6l!pCn6l!pCn6l!pCn6l!pCan6l!pCn6l!pCn6l!pCn6l!pCn6l!pCn6l!pCn6l!pCn6l!pCn6l!pCn6l!pCan6l!pCn6l!pCn6l!pCn6l!pCn6ꘃyb`㮭5ni[Tq`g_} /ϑ;%>_\),;E{Gn?ӧ}ۧɌ.Ɍ.]8х]8х](!p2 '3p2 '3PҢ`fť.E03pCť.E03fFn.E03fF E03fF"]!fF"]3 7D"]3`ft"]3`fť.]3`fť.х`fť.E03pCť.E03fFjZť.E03fFn.E03fF E03fF"]!fF"]3 7D"]3`ft"]3`fť.]3`fť.х`fť.E03pCť.E03fFn.E03fFBM.E03fF E03fF"]!fF"]3 7D"]3`ft"]3`fť.]3`fť.х`fť.E03pCť.E03fFn.E03fF E03fFBM.E03fF E03fF"]!fF"]3 7D"]3`ft"]3`fť.]3`fť.х`fť.E03pCť.E03fFn.E03fF E03fF"]iE03fF"]!fF"]3 7D"]3`ft"]3`fť.]3`fť.х`fť.E03pCť.E03fFn.E03fF E03fF"]!fF"]3 5-fF"]3 7D"]3`ft"]3`fť.]3`fť.х`fť.E03pCť.E03fFn.E03fF E03fF"]!fF"]3 7D"]3`ftE"]3`ft"]3`fť.]3`fť.х`fť.E03pCť.ꨂyDbq:-",{~.]|l?]wTQ'e\_Nxߏ1{F 3jOcy4 fl[{ai? %J3fia %J3fia %J3fa 7n3fia 7n3fia 7n3fia 7n3fia 7n3fia 7n3fia 7n3fia 7n3fia 7n3fia 7n3fa 7n3fia 7n3fia 7n3fia 7n3fia 7n3fia 7n3fia 7n3fia 7n3fia 7n3fia 7n3fa 7n3fia 7n3fia 7n3fia 7n3fia 7n3fia 7n3fia 7n3fia 7n3fia 7n3fia 7n30 a 7n3Դ0 a 7n3Դ0 a 7n3Դ0 a 7n3Դ0 a 7n3Դ0 a 7n3Դ0 a 7n3Դ0 a 7n3Դ0 a 7n3Դ0 a 7n30 a 7n3Դ0 a 7n3Դ0 a 7n3Դ0 a 7n3Դ0 a 7n3Դ0 a 7n3Դ0 a 7n3Դ0 a 7n3Դ0 a 7n3Դ0 a 7n30 a 7n3Դ0 a 7n3Դ0 a 7n3Դ0 a 7n3Դ0 a 7n3Դ0 a 7n3Դ0 a 7n3Դ0 a 7n3Դ0 a 7n3Դ0 a 7n30 a 7n3Դ0 a 7n3Դ0 a 7n3Դ0 a 7n3Դ0 ao53naƸv#ia[T0g_~ 2*̨@z%x8G>]%zCtjǾ}JB х %DFZtB х %DFZtB х %DFzt х 7DjZt х 7DjZt х 7DjZt х 7DjZt х 7DjZt х 7DjZt х 7DjZt х 7DjZt х 7DjZt х 7Dfzt х 7DjZt х 7DjZt х 7DjZt х 7DjZt х 7DjZt х 7DjZt х 7DjZt х 7DjZt х 7DjZt х 7Dfzt х 7DjZt х 7DjZt х 7DjZt х 7DjZt х 7DjZt х 7DjZt х 7DjZt х 7DjZt х 7DjZt х 3=pCt х 5-pCt х 5-pCt х 5-pCt х 5-pCt х 5-pCt х 5-pCt х 5-pCt х 5-pCt х 5-pCt х 3=pCt х 5-pCt х 5-pCt х 5-pCt х 5-pCt х 5-pCt х 5-pCt х 5-pCt х 5-pCt х 5-pCt х 3=pCt х 5-pCt х 5-pCt х 5-pCt х 5-pCt х 5-pCt х 5-pCt х 5-pCt х 5-pCt х 5-pCt х 3=pCt х 5-pCt х 5-pCt х 5-pCt х 5-pCtaWGS~ mE5ekbӢ㷨]|l?]l-~ČwtQ7k>DgdB5# %DFZdB kFJ,B " %׌,Y鑅" 7DnUsdBM,Y!ps Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd " 7Dn,Դ " 7Dfzd " 7DjZd " 7DjZd " 7DjZd " 7DjZd " 7DjZd " 7DjZd " 7DjZd " 7DjZd " 7DjZd " 7Dfzd " 7DjZd " 7DjZd " 7DjZd " 7DjZd " 7DjZd " 7DjZd " 7DjZd " 7DjZd " 7DjZd " 7Dfzd " 7DjZd " 7DjZd " 7DjZd " 7DjZd " 7DjZd " 7DjZd " 7DjZd " 7DjZd " 7DjZd " 7Dfzd " 7DjZd " 7DjZd " 7DjZd " 7DjZd̯&@dw,%ە Uşrd/ܮ?\׿|*/'Wg -'{.[z8뇇p,oc>-Nf8dNf8ᄓN8ᄓN8 '3p2 '3p2 %-fp"N3 7p"N3É`f8p"N3É`f8 'N3É`f8 '‰`f8 'D03pC8 'D03fn'D03fp D03fp"Np"N3É`f8 'N3É`f8 'ᄚN3É`f8 '‰`f8 'D03pC8 'D03fn'D03fp D03fp"N!fp"N3 7p"N3É`f8p"N3É`f8 '3fp"N3 7p"N3É`f8p"N3É`f8p"N3É`f8 'N3É`f8 '‰`f8 'D03pC8 'D03fn'D03fp D03fp"N!fp"N3 7 'D03fp D03fpBM 'D03fp D03fp"N!fp"N3 7p"N3É`f8p"N3É`f8 'N3É`f8 '‰`f8 'D03pC8 'D03fn'D03fp D03fp"NiD03fp"N!fp"N3 7p"N3É`f8p"N3É`f8 'N3É`f8 '‰`f8 'D03pC8 'D03fn'D03fp D03fp"N!fp"N3 5-fp"N3 7p"N3É`f8p"N3É`f8 'N3É`f8 '‰`f8 'D03pC8 'D03fn'D03fp D03fp"N!fp"N3 7p"N3É`f8p"N3É`f8p"N3É`f8 'N3É`f8 '‰`f8 'D03pC8 '긁yrpO '~N9?;˯_%xd"ן~>bkާZ\|Ǘ_[foj@c9|E1 hO{}? %J4hi %J4hi %J4h遆 7n4hi 7n4hi 7n4hi 7n4hi 7n4hi 7n4hi 7n4hi 7n4hi 7n4hi 7n4h遆 7n4hi 7 4h!P 7nh!pCn43pC@CM 4h9f@ h!ps@  5-pC@  5-pC@  5-pC@  5-pC@  3=pC@  5-pC@ gjZ@  7 4Դ@  7nhi 7n4<3P 7n4h!P 7n4h!P 7n4h!P 7n4h!P 7n4h遆 7n4hi 7n4hi 7n4hi 7n4hi 7n4hi 7n4hi 7n4hi 7n4hi 7n4hi 7n4h遆 7n4hi 7n4hi 7n4hi 7n4hi 7n4hi 7n4hi 7n4hi 7n4hi 7n4hi 7n4h遆 7n4hi 7n4hi 7n4hi 7n4hi 7n4hi 7n4hi 7n4hi 7n4hi 7n4hi 7n4h遆 7n4hi 7n4hi 7n4hi 7n4hi 3:`@Chs?[| 4oD.|)W;eFƃҊKq}CXD<ǻP^;=s} %J(/B  %J(/B  %J(/  7n(/Դ  7n(/Դ  7n(/Դ  7n(/Դ  7n(/Դ  7n(/Դ  7n(/Դ  7n(/Դ  7n(/Դ  7n(/  7n(/Դ  7n(/Դ  7n(/Դ  7n(/Դ  7n(/Դ  7n(/Դ  7n(/Դ  7n(/Դ  7n(/Դ  7n(/  7na[ 7n(/+]ӵo_njkQxe9ҵol,~ø~mZ, 75^ӵYp yi߱bypޛFnj׹k<8VOk̓{3?[Zn5;vsVJխINעuV,ii5>K_@dZ0qsʤj"c+za3g W+&V 2fZ0q9^iZ0qaj"s -D=LiZH^@d,W״^ aQjj|Zqaj" -D sj"kԛ>^qFeZV RoK -D j"W|0 -D W+aZV RkZV 2nj"{Rcj"\Ft-o:~CW+L -D=V 2L_@zqaj"3zC -75'V R?%W+W_@Ҽ^@dZȸ5ߖ41W+Z0qaj"rZV RoO -Dӻ^*=V 2L_@dZȵqaj"XѮiw W+_֫ W+&V rJ -Dj" -DDW+&V R?.)W+fK -DV%V 2L_@dZȵqaj"?i5Wô^@dZ0qkj" -Dq֫G_=?16V 2qaj" -Dj"ü\| -Dۏzk}j"cza^@^qکL -D\cL -Dꇱj"۷p_Ê?}PW/S -P=m@Ǿ}z΀B  -%FZ@B  -%FZ@B  -%Fz@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7fz@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7fz@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -3=pC@ᆀ  -5-pC@ᆀ  -5-pC@ᆀ  -5-pC@ᆀ  -5-pC@ᆀ  -5-pC@ᆀ  -5-pC@ᆀ  -5-pC@ᆀ  -5-pC@ᆀ  -5-p΀  -7fz@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ  -7jZ@ᆀ sn(Դ  -7 (Pi -7nP!P -7n(ܜ3pC@n(P!pC@n(P!pC@an(P!pC@n(P!pC@n(P!pC@n(P!pC@n(P!pC@n(P!pC@n(P!pC@n(P!pC@n(P!pC@n(P!pC@an(P!pC@n(P!pC@n(P!pC@n(P!pC@n(؁y -W@1T7-G (>~}DE%b8qϱ'j1=S} %J'pB  %J'pB  %J'p  7n'Դp  7n'Դp  7n'Դp  7n'Դp  7n'Դp  7n'Դp  7n'Դp  7n'Դp  7n'Դp  7n'p  7n'Դp  7n'Դp  7n'Դp  7n'Դp  7n'Դp  7n'Դp  7n'Դp  7n'Դp  7n'Դp  7n'p  7n'Դp  7n'Դp  7n'Դp  7n'Դp  7n'Դp  7n'Դp  7n'Դp  7n'Դp  7n'Դp  7fz8p  7jZ8p  7jZ8p  7jZ8p  7jZ8p  7j 7n'N!P 7n'N!P 7n'N!P 7n'N!P 7n'N!0 7n'N!P 7n'N!P 7n'N!P 7n'N!P 7n'N!PsN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄙN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚp  7n'Դp  7n'Դp  7n'Դp  7n'Դp  7n'p  7n'Դp  7n'Դp  7n'Դp  7n'Դp ᄙ_70O.pNw#i_É3>;?[q eϯט>}_O1U z,b -S\{{O >SuJ)S(!0b -%J)S(!0b -%J)S(!0c -7n)S!Pb -7n)S!Pb -7n)S!Pb -7n)S!Pb -7n)S!Pb -7n)S!Pb -7n)S!Pb -7n)S!Pb -7n)S!Pb -7n)S!0c -7n)S!Pb -7n)S!Pb -7n)S!Pb -7n)S!Pb -7n)S!Pb -7n)S!Pb -7n)S!Pb -7n)S!Pb -7n)S!Pb -7n)S!0c -7n)S!Pb -7n)S!Pb -7n)S!Pb -7n)S!Pb -7n)S!Pb -7n)S!Pb -7n)S!Pb -7n)S!Pb -7n)S!Pb -7n)S1b -7n)Si1b -7n)Si1b -7n)Si1b -7n)Si1b -7n)S9)S!pCLᆘBM)S!pCLᆘBM)S!pCLᆘBM)S!pCLᆘBM)S!pCLᆘL)S!pCLᆘBM)S!pCLᆘBM)S!pCLᆘBM)S!pCLᆘBM)S!pCLᆘBM)S!pCLᆘBM)S!pCLᆘBM)S!pCLᆘBM)S!pCLᆘBM)S!pCLᆘL)S!pCLᆘBM)S!pCLᆘBM)S!pCLᆘBM)S!pCLᆘBM)S!pCLᆘBM)S!pCLᆘBM)S!pCLᆘBM)S!pCLᆘBM)SgLᆘ 1S!pϘ 1b -3=pCLf1b -7jZLᆘM}sLᆘ 1S!pCLᆘ 1S!pCLᆘ 1S!0#i?S6b{-wc)~S\9_ÉT2>/ob3~|,xCd1^EyO>SxFJ,Y(!0" %DJ,Y(!0" %DJ,Y(!0# 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y!0# 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y!0# 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y!P" 7Dn,Y鑅" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Y鑅" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Y鑅" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Y鑅" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 7Dn,Yi" 3:`ro-o#-Fw,>|wd9_/A9b:k+fǗ/TqQB悺j+,7M=?_1go4:k7{5^NV/R'5>~'5c -::ta>;kn-ƒ:pދ,u 5޵ -qRýs2]PԺ6Xj]>S#~0MԺ`,4zpZYau6p!Zl -WbuxI3gu6}Әk]m`jX%ScުƼ{[yަk׹oLضt-Q ~WHb1[t-y]Z5rLckWG)uZ5{]ӵ8jK|?{GSc~;̻kQ>nӵio]G5hMע&}ikQ3߾ł1+]Lע&Bdjx rkQ^uY25WLydj̵ -{Sc>[n+-F$6𻔻W~ s,RSljuQW JD)=NkWˮv|6kqgK|?Zx<˾uޞ^/Kۜ:xyk۞:xw˽mPO{\[k{{l{^ݜg'K=uz~i^kmy׵`^nTO{\V.^[ܶY=uzi}X^{Gmzih^׶_=uzi;h_9mzmn^Gۯ:x{9x4;7WO{kNWO{\J~ܞ^KWO{\ڶ_=up^fWO{\\ۯ:m^@mzWs}ٟyN|y uyzny8uvyW{=mzbn{i^sZux9~W{<^mzzo^ܞ^\yԾ{<_=u8~yڞ^׶_=u^~W{<1h39ղo0㨧W{<<|w\￾^[sN϶g1.G׿Fc4N2It8ɀ% pN2It8ɀ% pN2It8ɀ%c@G1QLtd@td@G1QLta@G1QLtd@td@G1QLta@G1QLtd@td@G1QLta@G1QLtd@td@G1QLta@G1QLtd@td@G1QLt:ɀb2 (&:0 (&:ɀb2 :ɀb2 (&:0 (&:ɀb2 :ɀb2 (&:0 (&:ɀb2 :ɀb2 (&:0 (&:ɀb2 :ɀb2 (&:0 (&:ɀb2CQLtd@G1d@G1QLtnQLtd@G1d@G1QLtnQLtd@G1d@G1QLtnQLtd@G1d@G1QLtnQLtd@G1d@G1QLt:ɀb2 (&:0 (&:ɀb2 :ɀb2 (&:0 (&:ɀb2 :ɀb2 (&:0 (&:ɀb2 :ɀb2 (&:0 (&:ɀb2 :ɀb2 (&:0 (&:ɀb2CQLtd@G1d@G1QLtnQLtd@G1d@G1QLtnQLtd@G1d@G1QLtnQLtd@G1d@G1QLtnQLtd@G1d@G1QLtjƀb2 (&:ɀ7 (&:ɀb2 pÀb2 (&:ɀ7 (&:ɀb2 pÀb2 (&:ɀ7 (&:ɀb2 pÀb2 (&:ɀ7 (&:ɀb2 pÀb2 (&:ɀ7 (&:ɀb2 P3td@G1QLta@G1QLtd@td@G1QLta@G1QLtd@tj5ϰ_Š~~}YoްbFa'q:>?׻!o;9Oy1n:teB a -%FFXB a -%FFXB a -%FfXᆰ a -7jFXᆰ a -7jFXᆰ a -7jFXᆰ a -7jFXᆰ a -7jFXᆰ a -7jFXᆰ a -7jFXᆰ a -7jFXᆰ a -7jFXᆰ a -7ffXᆰ a -7jFXᆰ a -7jFXᆰ a -7jFXᆰ a -7jFXᆰ a -7jFXᆰ a -7jFX朰 a -7jFX朰 a -7jFX朰 a -7jFX朰 a -7ffX朰 a -7jFXᆰ a#a -5#pCXᆰ͑ aV!pCXHXᆰB+V!ps$pCXfn+V!pCXfn+V!pCXfn+V!pCXfn+V!pCXfn+V!03 -7n+V!P3 -7n+V!P3 -7n+V!P3 -7n+V!P3 -7n+V!P3 -7n+V!P3 -7n+V!P3 -7n+V!P3 -7n+V!P3 -7n+V!03 -7n+V!P3 -7n+V!P3 -7n+V!P3 -7n+V!P3 -7n+V!P3 -7n+V!P3 -7n+V!P3 -7n+V!P3 -7n+V!P3 -7n+V!03 -7n+V!P3 -7n+V!P3 -7n+V!P3 -7n+V!P3 -7n+V!P3 -7n+V!P3 -7n+V!P3 -7n+V!P3 -7n+V!P3 -7n+V!03 -7n+V!P3 -7n+V!P3 -7n+V!P3 -7n+V!P3 -7f~G+~XXq;o-_Ͱ⛟#xӣ%G޿Dߎ,<×O?EӸ<e\rc__‰,Y(!PBdadDJ,Y(!PBdadDJ,Y(!PBdadFn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdafFn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdafFn,Y9Y!P3" 7Dn,,Y" 7DnDn,Ԍ " 7G" 7DjFd #" 5#pCd " 5#pCd " 5#pCd " 5#pCd " 5#pCd Yy~<# 7Dn,Ԍ)" 7Dn,Ԍ)" 7Dn,Ԍ)" 7Dn,Ԍ)" 7Dn,Ԍ " 7Dn,Ԍ " 7Dn,Ԍ " 7Dn,Ԍ " 7Dn,Ԍ " 7Dn, " 7Dn,Ԍ " 7Dn,Ԍ " 7Dn,Ԍ " 7Dn,Ԍ " 7Dn,Ԍ " 7Dn,Ԍ " 7Dn,Ԍ " 7Dn,Ԍ " 7Dn,Ԍ " 7Dn, " 7Dn,Ԍ " 7Dn,Ԍ " 7Dn,Ԍ " 7Dn,Ԍ " 7Dn,Ԍ " 7Dn,Ԍ " 7Dn,Ԍ " 7Dn,Ԍ " 7Dn,Ԍ " 7Dn, " 7Dn,Ԍ " 7Dn,Ԍ " 7Dn,Ԍ " 7Dn,Ԍ Fd7"?Y܏}$b_{7?GdW,>=OY| +VwLtfX1.۸<e\+$pI -%NV8IX$aJ+$pI -' +$(&aE1 +V$(&aŠbV$pCXQLŠbVn+IXQLŠbV aE1 +IXQLŠbV!(&aE1 +IXQL -7$(&aE1 +IXᆰ$(&aE1 +V$(&aV$(&aŠbV$pCXQLŠbVn+IXQLŠbV aE1 +IXQLŠbV!(&aE1 +IXQL -7$(&aE1 +IXᆰ$(&aE1 +V$(&aŠbV$P3ŠbV$pCXQLŠbVn+IXQLŠbV aE1 +IXQLŠbV!(&aE1 +IXQL -7$(&aE1 +IXᆰ$(&aE1 +V$(&aŠbV$pCXQLŠbVB+y~<ŠbV aE1$(&aŠbNaE1 +IXQL -7Ŝ>ŠbVn+9}$(V aE1 +IXQ#(&aŠbVGXQL -7$(&aE1n+IXQLŠbnaE1 +V$(&aV$(&aŠbV$pCXQLŠbVn+IXQLŠbV aE1 +IXQLŠbV!(&aE1 +IXQL -7$(&aE1 +IXᆰ$(&aE1 +V$(&aŠbV$P3ŠbV$pCXQLŠbVn+IXQLŠbV aE1 +IXQLŠbV!(&aE1 +IXQL -7$(&aE1 +IXᆰ$(&aE1 +V$(&aŠbV$pCXQLŠbVjFXQLŠbVn+IXQLŠbV aE1 +IXQLŠbV!(&aE1 +IXQL -7$P8L6FXsŠUu}ݎ_^XOV9?׷,jhnV㯷_\?|/)X4.G׿|K; %J/B  %J/B  %J/  7n/Ԍ  7n/Ԍ  7n/Ԍ  7n/Ԍ  7n/Ԍ  7n/Ԍ  7n/Ԍ  7n/Ԍ  7n/Ԍ  7n/  7n/Ԍ  7n/Ԍ  7n/Ԍ  7n/Ԍ  7n/Ԍ  7n/Ԍ  7n/Ԍ  7n/Ԍ  7n/Ԍ  7n/  7n/Ԍ  7n/Ԍ  7n/Ԍ  7n/Ԍ  7n/Ԍ  7n/Ԍ  7n/Ԍ  7n/Ԍ  7n/Ԍ  7ff|  7jF|  7jF|  7jF|  7j.n/_%pC|fn/_%pC|fn/_%pC|fn/_%pC|fn/_%pC|fn/_!pC|afn/_!pC|fn/_!pC|fn/_!pC|fn/_!pC|B|  7jF|  7jF|  7jF|  7jF|  7jF|  7ff|  7jF|  7jF|  7jF|  7j.n/_!pC|fn/_!pC|fn/_!pC|fn/_!pC|fn/_!pC|fn/_!pC|afn/_!pC|fn/_!pC|fn/_!pC|fn/_!pC|B|# 2?_[o}YˈAoӣ}/[db_zy/" -.(nqy>ʸǾD#PBDB Q(!PBDB Q(!PBDB Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ Q!pCDᆈ̌(Q!pCDᆈB͈(Q!pCDᆈB͈(Q!pCDᆈB͈(Q!pCDᆈBͅ " -7Dn(Ԍ " -7Dn(Ԍ " -7Dn(Ԍ " -7Dn(Ԍ " -7Dn(Ԍ " -7Dn(̈ " -7Dn(Ԍ " -7Dn(Ԍ " -7Dn(Ԍ " -7Dn(Ԍ " -7Dn(Ԍ " -7Dn(Ԍ " -7Dn(Ԍ " -7Dn(Ԍ K" -7Dn(Ԍ K" -7Dn(̈  " -5#pCD撈 " -5#pCD撈 " -5#pCDᆈ " -5#pCDᆈ " -5#pCDᆈ " -5#pCDᆈ " -5#pCDᆈ " -5#pCDᆈ " -5#pCDᆈ " -33pCDᆈ " -5#pCDᆈ " -5#pCDᆈ " -5#pCDᆈ " -5#pCDab6XexݿzU do4!!w_rݧ\o_or|<˗O?k9|qk}}]PByB 兑Q^(PByB 兑Q^(PByB 兑Y^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 兙Y^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 兙Y^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy,/P^pCyB(/P^pCyB(/P^pCyB(/P^pCyB(/P^pCyB(/P^pCyB(/P^pCyB(/P^pCyB(/P^pCyB(/P^pCy,/P^pCyB(/P^pCyB(/P^pCyB(/P^pCyB(/P^pCyB(/P^pCyB(/P^pCyB(/P^pCyB(/P^pCyB(/P^pCy,/P^pCyB(/P^pCyB(/P^pCyB(/P^pCyB(/P^pCyB(/P^pCyB(/P^pCyB(/P^pCyB(/P^pCyB(/P^pCy,/P^pCyB(/P^pCyB(/P^pCyB(/P^pCyB(/P^a^AF|9cž%b[*/Y^|C>XϮZ gd=ˋq?.(Gy$兓NR^(pI ')/PBy$兓NR^8IydŤ(&E1)/Iy򢘔Ť(&E1)/P^򢘔Ť(&ʋbR^򢘔ŤpCyQLʋbR^򢘔n(/IyQLʋbR^ E1)/IyQLʋbR^(&E1)/IyQL 7Ť(&E1)/Iy򢘔Ť(&E1)/Ԍ򢘔Ť(&E1)/P^򢘔Ť(&ʋbR^򢘔ŤpCyQLʋbR^򢘔n(/IyQLʋbR^ E1)/IyQLʋbR^(&E1)/IyQL 7Ť(&E1)/Iy򢘔Ť(&E1)/P^򢘔Ť(&党Q^򢘔Ť(&ʋbR^򢘔ŤpCyQLʋbR^򢘔n(/IyQLʋbR^ E1)/IyQLʋbR^(&E1)/IyQL 7Ť(&E1)/Iy򢘔Ť(&E1)/P^򢘔Ť(&ʋbR^򢘔jFyQLʋbR^򢘔n(/IyQLʋbR^ E1)/IyQLʋbR^(&E1)/IyQL 7Ť(&E1)/Iy򢘔Ť(&E1)/P^򢘔Ť(&ʋbR^򢘔ŤpCyQLʋbR^򢘔n(/IyQLʋbR^B(/IyQLʋbR^ E1)/IyQLʋbR^(&E1)/IyQL 7Ť(&E1)/Iy򢘔Ť(&E1)/P^򢘔Ť(&ʋbR^򢘔ŤpCyQLʋbR^򢘔n(/IyQLʋbR^ E1)/IyQLʋbR^E1)/IyQLʋbR^(&E1)/IyQL 7Ť(&E1)/Iy򢘔Ť(&E1)/P^򢘔Ť(&ʋbR^򢘔ŤpCyQLʋbR^򢘔n(/IyQLʋbR^ E1)/IyQLʋbR^(&E1)/IyQL 5(&E1)/IyQL 7Ť(&E1)/Iy򢘔Ť(&E1)/P^s(/IyQLʋbR^(Q^a^AF|9ScqǑ*/⻟f^^|~߽ad?O?e(‚}}PBaB QX(PBaB QX(PBaB YXpCa QXpCa QXpCa QXpCa QXpCa QXpCa QXpCa QXpCa QXpCa QXpCa YXpCa QXpCa QXpCa QXpCa QXpCa QXpCa QXpCa QXpCa QXpCa QXpCa YXpCa QXpCa QXpCa QXpCa QXpCa QXpCa QXpCa QXpCa QXpCa QXpCa,,PXpCaB(,PXpCaB(,PXpCaB(,PXpCaB(,PXpCaB(,PXpCaB(,PXpCaB(,PXpCaB(,PXpCaB(,PXpCa,,PXpCaB(,PXpCaB(,PXpCaB(,PXpCaB(,PXpCaB(,PXpCaB(,PXpCaB(,PXpCaB(,PXpCaB(,PXpCa,,PXpCaB(,PXpCaB(,PXpCaB(,PXpCaB(,PXpCaB(,PXpCaB(,PXpCaB(,PXpCaB(,PXpCaB(,PXpCa,,PXpCaB(,PXpCaB(,PXpCaB(,PXpCaB(,PXa.AF -}T^a,,ifaqEa!}8⯿X.FSyOu\2} 񱯯J(/P^(02 %J(/P^(02 %J(/P^(02 7n(/P^P3 7n(/P^P3 7n(/P^P3 7n(/P^P3 7n(/P^P3 7n(/P^P3 7n(/P^P3 7n(/P^P3 7n(/P^P3 7n(/P^03 7n(/P^P3 7n(/P^P3 7n(/P^P3 7n(/P^P3 7n(/P^P3 7n(/P^P3 7n(/P^P3 7n(/P^P3 7n(/P^P3 7n(/P^03 7n(/P^P3 7n(/P^P3 7n(/P^P3 7n(/P^P3 7n(/P^P3 7n(/P^PspCy  5pCy  5pCy  5pCy 兙Y^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 兙Y^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 兙Y^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^pCy 兙Y^pCy 党Q^pCy 党Q^pCy 党Q^pCy 党Q^0ü>Fysʋ}~./⛟f/ʋO㟏oųY񯿵؇3TbpSqQ)>uC %J %FFC %J %FFC %J %Ff %J 7jF %J 7jF %J 7jF %J 7jF %J 7jF %J 7jnn(1PbpCfn(1PbpCfn(1PbpCfn(1PbpCafn(1PbpCfn(1PbpCfn(1PbpCfn(1PbpCfn(1PbpCfn(1PbpCF %J 7jF %J 7jF %J 7jF %J 7ff %J 7jF %J 7jF %J 7jF %J 7jF %J 7jF %J 7jnn(1PbpCfn(1PbpCfn(1PbpCfn(1Pb03K 7n(1PbP3J 7n(1PbP3J 7n(1PbP3J 7n(1PbP3J 7n(1PbP3J 7n(1PbP3J 7n(1PbP3J 7n(1PbP3J 7n(1PbP3J 7n(1Pb03K 7n(1PbP3J 7n(1PbP3J 7n(1PbP3J 7n(1PbP3J 7n(1PbP3J 7n(1PbP3J 7n(1PbP3J 7n(1PbP3J 7n(1PbP3J 7n(1Pb03K 7n(1PbP3J 7n(1PbP3J 7n(1PbP3J 7n(1PbP3J 7n(1PbP3J 7n(1PbP3J 7n(1PbP3J 7n(1PbP3J 7n(1PbP3J 7n(1Pb03K 7n(1PbP3J 7n(1PbP3J 7n(1PbP3J 7n(1PbP3J 7f~_Q(1~XBwsmO3K/JOFʋ_^./EƷJח.b^ϧ.\4?u3?[ޢŤpCrQLb]ꢘdn.IxQLʋb^͈/J}P_4 -`4D ( t %h(%FCI1 -h(1FC1JPz2JPd4( e4. (4 %h(uFC3 -h(FC)4JP4JP2i4P Jj4V p4rk4` %(fD h(FC6 -"h(FCI7JP⍂7JPp4 p4T %( h()GCi9 -"h(5GC9JP(:JPu4 u4 %( h(yGC; %h(GC< -h(GC<JPJH=JPbR{4ܣ z|4⣡$ (裡T %h(GC ? -h(GCi?JPꏂ?JPR4 4 -d ) %i(-HCA -i(9HCAJh! % i(MHCBJRYHCBJPʐDP␆R4i(IA' %@i(JCIPJRJCPJP:DPRҢ4(4) Hi(IJA4) %Ji(UJCRJRaJC)SJPڔDPҧ4@*4FD* Ri(ѩ4P* %Ui(JA* Vi(JCUJRJCIVJPRDP4r+Ѯ4x32>W?g]_ֻo+Wyfʗ|yT#A_,w>֟)?b~?^"ocO`>?6r{ח7^__Lp~㙯ϝZkѼWev޿z}|'xk;<ۿs{uZOἿU?g +xMdI^y`|D H D4*9@׎\of5Tٙ~ٱx~yuǗ|/z=_?ח}r??ԯ_mr>,c7~~y}X8~lx{x>x?5L~wuԟ1Z}~?zox=9`[q'/|;>O?x?]^|۵?)tG·}xg{vlg;97o/|g˾^^˵=wwx2׬|}mmE +Zdl@^E]H x>@jqp~R]W <0W |oa^W w]_w zzp5zMR]3Gxyg`M8e#<G~XC^/+ xY><eޯ4o~Gz.Ҙ*Ԙ띞|=\5x1_|OLyҘsq֘_ۖ:_8k̯ޟs֘ם\Z鹨YxϔQ>sQޚ\w?\wcsM}Ƕ=sQs߱Wz.j;}]}o]Y\wgsMDZi5)}OEMǹ`jiyzuy~i^kgZ5֫{̓-WM\e25+n5wMk\SwZ5ޯQG̓֫G̓35^=j|_qy}գW֫G̓Ҙk|qypzyG̓w_=j\iz<~izyp-T57*zXxuX&/2^=5mØGyƼYc1q|֘z>k몘1qjg_=Wϭ|յBX7)sטzw_=-E;{WϽQc~iz55ɥ15w_=;//w+=k\&WEzypݚ45.WϳƼ1oquB1qj:11qkG_],Wϻ|1q|՘ueM|Uc1q|՘︿zkw_=k\֫g̓W_=k|YW︿zY{Y{s9ޯ}-+|?{[?v5?mX=a/r}3}bOz*!9*JZ[C\bˬo?-e={{X^">嵆Ʋ0ջ~pG!:Y=ڶr{G{:5x<khY\dz|?4}g*Y#:ml2kkx1זƳ1k#x=s`/f`|3f.nd0kzKטw]Yc^_85";x5NYWc{ϱ|'5Xc>׀x֘׍s1W~s1Y-]xx5{8x֘א4kuG2k2_j4X\7|_jNux|b  5s[35yx^=g̀-85x=xxƳ&")5~iok\_Ƴۚk;xOQzuy-?EڙƳk5dOywY`[Ϛk{\iokwokik\#k| >]׋!_ZHZtypypʜ_k{xW#mVxt}ߓ`Xx[cM +ur=*0xlk# +jlVɬ1)Nטx܏-]XwDgǯft}ڷt_㵖 i<~f_2kkO*0ǕX w>W|t__\nx5m +JU`<#]x9V;W\Olڃ㹖6 +g4{'|ky{*0Mn^k'|`֘_`xMǯc}u=}>INXf-x2-Wb>WsTt}c[Kt_ƶt}c X閮U`lv{*0DN*0֪wO{eǏ$IxYc &5u=*0֤HU`lz?V2kXGڃct_X45#Wrt}cwǯc_z\ƾ^b=et}cW?=~{EqWnN +!w\q}~x0kL*0HU`iN*0إ{*0 +x֘+WIt}ct_Z߼ +"828MYZ+l +8lk{*0і`8aǯ<>ev0kt_ƚ [ƚ +s *]8+oOYyt}c{ǯcW>Wq;ɬ1CϚg{*0>zy-i'TtU|),:?}g|ǗOх.ttᤣ %DN:pх.ttIGN:pх.".". E0]E0]х`:`: 7DttLGttLGn.".".]E0]E0]!`:`:pCtLGttLGtt".". E0]E0]х]E0]E0]!`:`:pCtLGttLGtt".". E0]E0]х`:`: 7DttLGttLGn.".".]E0]E0]!`:`:P3`:`: 7DttLGttLGn.".".]E0]E0]!`:`:pCtLGttLGtt".". E0]E0]х`:`: 7DttLGttfDttLGttLGn.".".]E0]E0]!`:`:pCtLGttLGtt".". E0]E0]х`:`: 7DttLGttLGn.".".Ԍ".". E0]E0]х`:`: 7DttLGttLGn.".".]E0]E0]!`:`:pCtLGttLGtt".". E0]E0]х]E0]E0]!`:`:pCtLGttLGtt".". E0]E0]х`:`: 7DttLGttLGn.".".]E0]E0]!`:`:P3`:`: 7DttLGttLGn.".".]E0]E0]! 5?;O= /+kkfݧe,[~.^1b-?h|:)vƻ?yOS7bQ{OH]߅>e7D袦|[[t7]9G}]۷-̉Ś7k]dEn~x~/>K# %DJ,Y" %DJ,Y" %DJ,Y" 7Dn*5pCdfDn,Y;pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdafFn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdafFn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdfDn,Y!pCdEd " 7DjFd " 33pCd " 5#pCd " 5#pCd " 5#pCd " 5#pCd " 5#pCd " 5#pCd " 5#pCd " 5#pCd " 5#pCd " 33pCd " 5#pCd " 5#pCd " 5#pCd " 5#pCd " 5#pCd " 5#pCd " 5#pCd " 5#pCd " 5#pCd " 33pCd " 5#pCd " 5#pCd " 5#pCd " 5#pCd " 5#pCd " 5#pCd " 5#pCd " 5#pCd " 5#pCd " 33pCd " 5#pCd " 5#pCd " 5#pCd " 5#pCdagG)!eEu D*7?Eɑw~~רvrypres<Z|_[/js^+b 4x@c{gh_h(!PB@4h(!PB@4h(!PB@ 4h!pC@C4h!pC@C4h!pC@C4h!pC@C4h!pC@C4h!pC@C4h!pC@C4h!pC@C4h!pC@C4h!pC@ 4h!pC@C4h9;pC@C4h9;pC@C4h9;pC@C4h9;pC@C4h9;pC@C4h!pC@C4h!pC@C4h!pC@C4h!pC@ 4h!pC@C4h!pC݁h!pC@ͻ 5#pC@ wjF@  74Ԍ@  7n4Ԍ@  7n4Ԍ@  7n4Ԍ@  7n4Ԍ@  7ff@  7jF@  7jF@  7jF@  7jF@  7jF@  7jF@  7jF@  7jF@  7jF@  7ff@  7jF@  7jF@  7jF@  7jF@  7jF@  7jF@  7jF@  7jF@  7jF@  7ff@  7jF@  7jF@  7jF@  7jF@  7jF@  7jF@  7jF@  7jF@  7jF@  7ff@  7jF@  7jF@  7jF@  7jF@ώ5~=wzub[h_4]w}çT_;VZi|^u1߾?UZ|j_>+kQ>?w%WbԷfY?؏.1Pb(PBadJ(1Pb(PBadJ(1Pb(PBadn(1PbpCfn(1PbpCfn(1PbpCfn(1PbpCfn(1PbpCfn(1PbpCfn(1PbpCfn(1PbpCfn(1PbpCfn(1PbpCafn(1PbpCfn(1PbpCfn(1PbpCfn(1PbpCfn(1PbpCfn(1PbpCfn(1PbpCfn(1PbpCfn(1PbpCfn(1PbpCafn(1Pbpӽ,1PbpC%QbpCͻK 5pC %wjF %J 7.1Ԍ%J 7n(1Ԍ%J 7n(1Ԍ%J 7n(1Ԍ%J 7n(1Ԍ%J 7ff %J 7jF %J 7jF %J 7jF %J 7jF %J 7jF %J 7jF %J 7jF %J 7jF %J 7jF %J 7ff %J 7jF %J 7jF %J 7jF %J 7jF %J 7jF %J 7jF %J 7jF %J 7jF %J 7jF %J 7ff %J 7jF %J 7jF %J 7jF %J 7jF %J 7jF %J 7jF %J 7jF %J 7jF %J 7jF %J 7ff %J 7jF %J 7jF %J 7jF %J 7jF̏(Pb2J\+1~o1KWJ>eϳvK2ߕ<]i,eAuJ_ƿ[iQ?_w+-~o1KWJ>姴v>şqg^׷8_Ryql'x{x254>INp ']^8INPBy ']^8IJFyLtyLty"./"./ E0]^E0]^ʋ`ˋ` 7tyLtyLn(/"./"./P^E0]^E0]^ˋ`ˋ`pCyLtyLty"./"./B(/"./"./P^E0]^E0]^ˋ`ˋ`pCyLtyLty"./"./ E0]^E0]^ʋ`ˋ` 7tyLtyLn(/"./"./P^E0]^E0]^E0]^E0]^ʋ`ˋ` 7tyLtyLn(/"./"./P^E0]^E0]^ˋ`ˋ`pCyLtyLty"./"./ E0]^E0]^ʋ`ˋ`P3ʋ`ˋ`Sy"./"./yT^ˋ`ˋ`^?n(/"./"Oʋ`ˋ` 7tyLtyLn(/"./"./P^E0]^E0]^ˋ`ˋ`pCySyLtyLjFySyLtyLn(/|ʋ`ˋ`pCySyLtyLn(/~*/"./ E0]^E0]^ʋ`ˋ` 7tyLtyLn(/"./"./P^E0]^E0]^ˋ`ˋ`P3ʋ`ˋ` 7tyLtyLn(/"./"./P^E0]^E0]^ˋ`ˋ`pCyLtyLty"./"./ E0]^E0]^ʋ`ˋ` 7tyLtyLjFyLtyLty"./"./ E0]^E0]^ʋ`ˋ` 7tyge.XX7~S~ʋ⸾E)ޤ]^+nx{>x_SOz^C%GwĴ?KIKvMI xW Z%5U'Jj糖OFjZ- &uApk_kWx>Z'3ߟwI[d 9=^j^兒8P}'Y~΅gYnjﺲsՂh55ܖ,ޯ4jHc>jJYc~m[z.|1Gz. ^75u1פ==5/=5LE{k6sQ߱sQ3My鹨o\wl +75}{碦ue9sQQX25q75sQSQk`j<74Ԙז1_]\l5gM\l5;<[̓G3yx?^i5#-N+NϵlV8ty|uyp-4畖[̓kq֨[̓72?"WZn5#-S51zuy^j<_ϴ^j<_GZn5V#mNZn5?^j<^j[ZngҘkDz.j\_izej" +DW+0j"z\ze>?,JM̓;V r_qej"W+Y&V ̝֫|ߟwW+oTj"6^@dZX֫˜+Y&V L_@^ô^@״^@d +D94g܏3V L_=?o:j}~DZV ֫̕,W+Y{_@dZx +DxJ +DR1j"]zT֫,W+Y3y̓ +D֢.V L_@dZȽ֫[ze^]/#V L_@dZo=qˆ,W+Y&V R?,W+Yb75;V roZH8W+w_@~luZV ̝֫#V R;iZ2q{MiZH0^@dwZV R?21ɼL~j̓_=aZAj"~ +D94 +DGj" +DR֫,sj"Դ^@dZqJzej"˼zuC|1{~v @_"G~'9W?ȱO R? ~z25O r~#k rCNh^׏.U{"r5Cz r-"~oQ0SNj~B/_;S7N7w|;Ͽs'XY?e5>C p(%ad¡S8p +NP)F)J8C p(%adS8p +Np)j)n8 p7¡fS8p +Np)j)n8 p7¡fS8p +Np)j)n8 p7¡fS8p +Np)j)n8 p7¡fS8p +Np)j)n8 p7afS8p +Np)j)n8 p7¡fS8p +Np)j)n8 p7¡fS8p +Np)j)n8 p7¡fS8p +Np)j)n8 p7¡fS8p +Np)j)n8 p7afS8p +Np)j)n8 p7¡fS8p +Np)j)n8 p7¡fS8p +Np)j)n8 p7¡fS8p +Np)j)n8 p7¡fS8p +Np)j)n8 p37S8p +NP3Np)n8 p57S8p +NP3Np)n8 p57S8p +NPsr +Np)n8 pp7S8p +q +Np)n8 pp7S8p +q +Op)n8 pp7S8p +q +Np)n8 pp7S8p +q +Np)n8 pp7S8p +S8p +Nps)n8C8 p7WS8ԌS8p +Nps)n8C8 p7S8ԌS8p +Np)n8< p7S8ԌS8p +Np)n8C8 p7S8ԌS8p +Np)n8C8 p7S8S8p +Np)j)n8 p7¡fS8p +Np)j)n8 p7¡fS8p +Np)f)n8 p7¡fS8p +Np)j)n8 p7¡fS8p +Np)j)n8̏_?CYwޯڎ7Y)? -]o~~<)O \ (x>|)_P(!PB@(P(!PB@(P(!PB@ (P!pC@ᆀB(P!pC@ᆀB(P!pC@ᆀB(P!pC@ᆀB(P!pC@ᆀB(P!pC@ᆀB(P!pC@ᆀB(P!pC@ᆀB(P!pC@ᆀB(P!pC@ᆀ (P!pC@ᆀB(P!pC@ᆀB(P!pC@ᆀB(P!pC@ᆀB(P!pC@ᆀB(P!pC@ᆀB(P!pC@ᆀB(P!pC@ᆀB(P!pC@ᆀB(P!pC@ᆀ (P!pC@ᆀB(P!pC@ᆀB(P!pC@ᆀB(P!pC@ᆀB(P!pC@ᆀB(P!pC@ᆀB(P!pC@ᆀB(P!pC@ᆀB(P!pC@ᆀB(P!pC@afn(P!pC@fn(P!pC@fn(P!pC@fn(P!pC@fn(P!pC@$pC@ᆀ  +5#pC@ᆀ  +5#pC@ᆀ  +5#pC@ᆀ  +5#pC@ᆀ  +33pC@ᆀ  +5#pC@ᆀ  +5#pC@ᆀ  +5#pC@ᆀ  +5#pC@ᆀ  +5#pC@ᆀ  +5#pC@ᆀ  +5#pC@ᆀ  +5#pC@ᆀ  +5#pC@ᆀ  +33pC@ᆀ  +5#pC@ᆀ  +5#pC@ᆀ  +5#pC@ᆀ  +5#pC@ᆀ  +5#pC@ᆀ  +5#pC@ᆀ  +5#pC@ᆀ  +5#pC@  +5#pC@  +33pC@  +5#pC@>5pC@ᆀB(P!pC@ᆀB(P!pC@ᆀB(Pٱ}o(~nU=W_P⧀w~:Q_w 'shp '֕gc?,;PB8pB ᄑN(!PB8pB ᄑN(!PB8pB ᄑN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄙN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄙN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p ᄚN!pC8p 'N!pC8pB'N!pC8pB'N!pC8pB'N!pC8pB'N!pC8pB'N!pC8pB'N!pC8pB'N!pC8pB'N!pC8pB'N!pC8p 'N!pC8pB'N!pC8pB'N!pC8pB'N!pC8pB'N!pC8pB'N!pC8pB'N!pC8pB'N!pC8pB'N!pC8pB'N!pC8p 'N!pC8pB'N!pC8pB'N!pC8pB'N!pC8pB'N!pC8pB'N!pC8pB'N!pC8pB'N!pC8pB'N!pC8pB'N!pC8p 'N!pC8pB'N!pC8pB'N!pC8pB'N!pC8pB'Nq}ro'~nD}[n_8‰+}O8~n-믿>kft2E)O2c?}6ebO7,.uuȂ}} +'PBdB Y(!PBdB Y(!PBdB Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd Y!pCd̎,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCd̎,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCd̎,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCd̎,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Ye4Aud!o,.v7?Gdŧ{{E?Ϡ#bbf`sgŒ[.tE1߱ڊ^o<?yGyvJ.C^hT2m *x>{~Jr/T\s\%sǼQ2{}] rorgps*;ۃcK(kwp_o42{ҭ<58|څ(}\}'</CϾ|n抟`17ng_plN_< /8Vkyg_p>@Ͼ|zG}_y}xۮy}1nS{,NsWfr<|}k:7r3|=cqk?z=%Y{,.sͷ=cخ2|G\qkuq::>cqk~cqu2lfD:xy=5:xy^4hӬ׷==:x}iۣmPO[kYﷶE=:xl.4՜_g'K=:x=mikuz,u؍iUW۪ft=f4zmӬUّͬ5:ibۯf/mzuziWO^_=:8)E{,f<^iYgۯf~tng c1m~B1o{,f<^iUiۯf<^85:x?̥OP{,fmzu~yci]˽Wϳޯmzu~yZۯg1h3sͳZ63|YzzuX~|u:HSW#9Vz:at$W#9L=_0|u:VW#:t$3~u:czjHRjf|t$磞NGrz:ӑNGr{ۯNGcHm:ɼ[W#9L=_0϶_>?\t$ӑNGr?:lH&mHD|u:\./|u:۵NGrzz}oZW t$t$ӑ{=_0|u:ۣNGrz:(_|n暟|u:/m:NG2_ۯNGrz:ޮXSW#96u|u:HSW#ӑ;~u:<ڻ^#_t$ӑNGrHSW#;S\󭞯NGrz:˶_0|u:Hǿt$~u:HNGrz:T_0_0S{+جz:q{=_dnoHS=_0|u:g<6lHSW#9L=_~HSW#/S~z쵞~NGr<1t$ӑ5焵t$y\󽞯NGr?=t$._~zH[t$t$[=_dN*~u:H2ӑlkHlH'+̛f߮:/ޯ:๿au:At$󥞯NGr˽]H't$ӑNGr?kHskH]m:at$ӑL9ӑNGrGۯNGr~ku߿:at$ ~u:HTjHs ߿H.߿H.zHGku2?WGr_}u$ɵ~ p{_[~HmɱlڱBuGH7?ű˞䗆ô秐|WC_?nLqم\~4$;.׹"ӟڞO:Ӏuݼe55cTSt(a@t(a@5C :0C :0СJСJadpÀ7 pÀ7 Pta@ta@tY:0 :0 :Ԭnnjր7 pÀ7 pÀ5k@ta@ta@5 :0 :0C͝nnjր7 pÀ7 pÀ5k@ta@ta@= :0 :0CnnСf pÀ7 pÀ7 Pta@ta@tY:0 :0 :Ԭnnjր7 pÀ7 pÀ5wta@ta@tY:0 :0 :Ԭnnf7 pÀ7 pÀ5k@ta@ta@5 :0 :0CnnСf pÀ7 pÀ7 Pta@ta@tY:0 :0 :nnСf pÀ7 pÀ7 Pta@ta@= :0 :0CnnСf pÀ7 pÀ7 Pta@ta@tY:0 :0 :Ԭnnjր7 pÀ7 pÀ5k@ta@ta@5 :0 :0CnnafpÀ7 pÀ7 Pta@ta@tY:0 :0 :Ԭnnjր7 pÀ7 pÀ5k@ta@ta@5 :0 :0CnnСf pÀ7 pÀ7 Pta@ta@t:0 :0 :Ԭnnjր7 pÀ7 pÀ5k@ta@ta@5 :0 :0CnnСf pÀ7 pÀ7 Pta@ta@tY:0 :0 :Ԭnnf7 pÀ7 pÀ5k@ta@ta@5 :0 :0CnnСf pÀ3_5ϰo =Š㯡Y6sX#}a7q7 ָq9xM뗿 1n: +avtnn^oY.$pI %DN]8It$хDJ.$pI '.袘D$(&E1.]袘D$(&хb]s.ItQL 7D$(]袘Dn.ItQ#(&E1.]袘GtQLb]!(&E1׏袘D$pCtQLb]袘Dn.ItQLb] E1.ItQLb]YE1.ItQLb]!(&E1.ItQL 7D$(&E1.It袘D$(&E1.]袘D$(&хb]袘D$pCtQLb]袘Dn.ItQLb] E1.ItQLb]!(&E1.ItQL 5+(&E1.ItQL 7D$(&E1.It袘D$(&E1.]袘D$(&хb]袘D$pCtQLb]袘Dn.ItQLb] E1.ItQLb]!(&E1.ItQL 7D$(&E1.Ԭ袘D$(&E1.]袘D$(&хb]袘D$pCtQLb]袘Dn.ItQLb] E1.ItQLb]!(&E1.ItQL 7D$(&E1.It袘D$(&E1.]袘D$(&х]袘D$(&хb]袘D$pCtQLb]袘Dn.ItQLb] E1.ItQLb]!(&E1.ItQL 7D$(&E1.It袘D$(&E1.]袘D$(&хb]袘D$Pb]袘D$pCtQLb]袘Dn.ItQLb] E1.ItQLb]!(&E1.ItQL 7D$(&E1.It袘D$(&E1.]袘D$(&хb]袘D$pCtQLb]袘DjVtQLb]袘Dn.ItQLb] E1.ItQLb]!(&E1.ItQL 7D$P˨L=Ft{wE!-3YtO]\kt>&ZGHo-fj+Zpb~cnnZOJ-Z(!0B %J-Z(!0B %J-Z(!0C 7n-Z!PB 7n-Z!PB 7n-Z!PB 7n-Z!PB 7n-Z!PB 7n-Z!PB 7n-Zy&PB 7n-Zy&PB 7n-Zy&PB 7n-Z-Z!pChB +-Z!pChB +-Z!pChB +-Z!pChB +-Z!pChB +-Z!pChB +-Z!pChB +-Z!pChB +-Z!pChB +-Z!pCh-Z!pChB +-Z!pChB +-Z!pChB +-Z!pChB +-Z!pChB +-Z!pChB +-Z!pChB +-Z!pChB +-Z!pChB +-Z!pChafn-Z!pChfn-Z!pChfn-Z!pChfn-Z!pChfn-Z!pChfn-Z!pChfn-Z!pChfn-Z!pChfn-Z!pChfn-Z!pChafn-Z!pChfn-Z!pChfn-Z!pChfn-Z!pChfn-Z!pChfn-Z!pChfn-Z!pChfn-Z!pChfn-Z!pChfn-Z!pChafn-Z!pChfn-Z!pChfn-Z!pChfn-Z!pChfn-Z!pChfn-Z!pChfn-Z!pChfn-Z!pChfn-Z!pChfn-Z!pChafn-Z!pChfn-Z!pChfn-Z!pChfn-Z!pChfn-|}<:Ǘ1۽W;7?Ghqŧ{{Mx?co'\~8^+,n?X?W)Onnc_ .\(!PBpadJ.\(!PBpadJ.\(!PBpadn.\!pCpfn.\!pCpfn.\!pCpfn.\!pCpfn.\!pCpfn.\!pCpfn.\!pCpfn.\!pCpfn.\!pCpfn.\!pCpafn.\!pCpfn.\!pCpfn.\!pCpfn.\!pCpfn.\!pCpfn.\!pCpfn .\!pCpfn .\!pCpfn .\!pCpfn .\!pCpafn .\!pCpfn.\'pCpfn.\'pCpfn.\'pCpfn.\'pCpfn.\!pCpfn.\!pCpfn.\!pCpfn.\!pCpfn.\!0 7n.\!P 7n.\!P 7n.\!P 7n.\!P 7n.\!P 7n.\!P 7n.\!P 7n.\!P 7n.\!P 7n.\!0 7n.\!P 7n.\!P 7n.\!P 7n.\!P 7n.\!P 7n.\!P 7n.\!P 7n.\!P 7n.\!P 7n.\!0 7n.\!P 7n.\!P 7n.\!P 7n.\!P 7n.\!P 7n.\!P 7n.\!P 7n.\!P 7n.\!P 7n.\!0 7n.\!P 7n.\!P 7n.\!P 7n.\!P 7f~S.~vo-qd!3o~ރOl?Z|;.x#8>~V<`>)x^_"|6a +%J+B a +%J+B a +%J+ a +7n+Ԭ a +7n+Ԭ a +7n+Ԭ a +7n+Ԭ a +7n+Ԭ a +7n+Ԭ a +7n+Ԭ a +7n+Ԭ a +7n+Ԭ a +7n+ a +7n+Ԭ a +7n+Ԭ a +7n+Ԭ a +7n+Ԭ a +7n+Ԭ a +7n+Ԭ a +7n+Ԭ a +7n+Ԭ a +7n+Ԭ a +7n+ a +7n+Ԭ a +7n+Ԭ a +7n+Ԭ a +7n+Ԭ a +7n+Ԭ a +7n+Ԭ a +7n+Ԭ a +7n+Ԭ a +7n+Ԭ a +7fvX+V!P +7n+V!P +7n+V!P +7n+V!P +7n+V!P +7n+V!P +7n+V!P +7n+V!P +7n+V!P +7n+V!0 +7n+V!P +7n+V!P +7n+V!P +7n+V!P +7n+V!P +7n+V!P +7n+V!P +7n+V!P +7n+V!P +7n+V!0 +7n+V!P +7n+V!P +7n+V!P +7n+V!P +7n+V!P +7n+V!P +7n+V!P +7n+V!P +7n+V!P +7n+V!0 +7n+V!P +7n+V!P +7n+V!P +7n+V!P +7f~G+~q#%V+)>ŠG+>k/y`? +Ws&:VͷuuקGX$aNV(!pI +' +$PBX$aNV8IXd$(&aE1 +IXᆰ$(&aE1 +V$(&aŠbV$pCXQLŠbVn+IXQLŠbV aE1 +IXQLŠbV!(&aE1 +IXQL +7$(&aE1 +IXᆰ$(&aE1 +Ԭ$(&aE1 +V$(&aŠbV$pCXQLŠbVn+IXQLŠbV aE1 +IXQLŠbV!(&aE1 +IXQL +7$(&aE1 +IXᆰ$(&aE1 +V$(&aV$(&aŠbV$pCXQLŠbVn+IXQLŠbV aE1 +IXQLŠbV!(&aE1 +IXQL +7$(&aE1 +IXᆰ$(&aE1 +V$(&aŠbVjVXQ,Vn+9}$(&aE1 +Vs+IXQLŠbV!(V$pCXQ#(&aE1n+IXQLŠbnaE1 +V>ŠbV!(&aE1 +}$pCXQLŠbVs+IXᆰ$(&aE1 +Ԭ$(&aE1 +V$(&aŠbV$pCXQLŠbVn+IXQLŠbV aE1 +IXQLŠbV!(&aE1 +IXQL +7$(&aE1 +IXᆰ$(&aE1 +V$(&aV$(&aŠbV$pCXQLŠbVn+IXQLŠbV aE1 +IXQLŠbV!(&aE1 +IXQL +7$(&aE1 +IXᆰ$(&aE1 +V$(&aŠbV$PŠbV$pCXQLŠbVn+IXQLŠbV aE1 +IXQLŠbV!(&a_d7Š+x[ߏaſ,+5|o +?>D?,.|,SdqY(!PBdȊ,Y(!PBdȊ,Y(!PBdȎ,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCd̎,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCd̎,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdB͊,Y!pCdafGn,Y!pCdfEn,Y!pCdfEn,Y!pCdfEn,Y!pCdBd [" 7DjVd [" 7DjVd [" 7DjVd [" 7DjVd [" 7DjVd " 7Dfvd " 7DjVd " 7DjVd " 7DjVd " 7Dj.Dn,Y!pCdfEn,Y!pCdfEn,Y!pCdfEn,Y!pCdfEn,Y!pCdfEn,Y!pCdafGn,Y!pCdfEn,Y!pCdfEn,Y!pCdfEn,Y!pCdBd " 7DjVd " 7DjVd " 7DjVd " 7DjVd " 7DjVd " 7Dfvd " 7DjVd " 7DjVd " 7DjVd " 7Dj.Dn,2 S:Ec#kadOYGm~~F_Ê+|VpvY7{Y}OV(!PBX ++V(!PBX ++V(!PBX+V!pCXᆰB ++V!pCXᆰB ++V!pCXᆰB ++V!pCXᆰB ++V!pCXᆰB ++V!pCXᆰB ++V!pCXᆰB ++V!pCXᆰB ++V!pCXᆰB ++V!pCXᆰ+V!pCXᆰB ++V!pCXᆰB ++V!pCXᆰB ++V!pCXᆰB ++V!pCXᆰB ++V!pCXᆰB ++V!pCXᆰB ++V!pCXᆰB ++V!pCXᆰB ++V!pCXᆰ+V!pCXᆰB ++V!pCXᆰB ++V!pCXᆰB ++V!pCXᆰB ++V!pCXᆰB ++V!pCXᆰB ++V!pCXᆰB ++V!pCXᆰB ++V!pCXᆰB ++V!pCXafn+V!pCXfn+V!pCXfn+V!pCXfn+V!pCXBXᆰ a +7jVXᆰ a +7jVXᆰ a +7jVXᆰ a +7jVXᆰ a +7jVXᆰ a +7fvXᆰ a +7jVXᆰ a +7jVXᆰ a +7jVXᆰ a +7jVXᆰ a +7jVXᆰ a +7jVXᆰ a +7jVXᆰ a +7jVXᆰ%a +7jVXᆰ%a +7fvXᆰ|GXᆰ aV!psIXᆰ aV!psIXᆰ aV!pCXᆰ aV!pCXᆰ aV!pCXᆰ aV!pCXᆰ aV!pCXᆰ aV!pCXᆰ aV!pCXᆰ aV!pCXᆰ aV!pCXᆰ aV!pCXᆰ aV!pCXᆰ aV!08L6FX{/ÊaXOV5+`QCfrb|1 n_+7{Y}OJ/_(!0 %J/_(!0 %J/_(!0 7n/_!P 7n/_!P 7n/_!P 7n/_!P 7n/_!P 7n/_!P 7n/_!P 7n/_!P 7n/_!P 7n/_!0 7n/_!P 7n/_!P 7n/_!P 7n/_!P 7n/_!P 7n/_!P 7n/_!P 7n/_!P 7n/_!P 7n/_!0 7n/_!P 7n/_!P 7n/_!P 7n/_!P 7n/_!P 7n/_!P 7n/_!P 7n/_!P 7n/_!P 7n/_ 7n/_Y 7n/_Y 7n/_Y 7n/_Y 7n/_Y 7n/_Y 7n/_Y 7n/_Y 7n/_Y 7n/_ 7n/_Y 7n/_Y 7n/_Y 7n/_Y 7n/_Y 7n/_Y 7n/_Y 7n/_Y 7n/_Y 7n/_ 7n/_Y 7n/_Y 7n/_Y 7n/<_Y 7n/<_Y 7n/<_Y 7n/<_Y 7n/_Y 7n/_Y 7n/_ 7n/_Y 7n/_Y 7n/_Y 7n/_Y 3,o[|1o8ZF _|S|O)8^\n?0n?Q<;X7ϧO}//OOяI" +'($PBD$DNQ8IDI" +'($pBɊ(IDQL"bQ E1(IDQL"bQ!(&E1(IDQL" +7D$(&E1(IDᆈD$(&E1(QD$(&"bQD$pCDQL"bQDn(IDQL"bQ E1(IDQL"bQYE1(IDQL"bQ!(&E1(IDQL" +7D$(&E1(IDᆈD$(&E1(QD$(&"bQD$pCDQL"bQDn(IDQL"bQ E1(IDQL"bQ!(&E1(IDQL" +5+(&E1(IDQL" +7D$(&E1(IDᆈD$(&E1(QD$(&"bQD$pCDQL"bQDn(IDQL"bQ E1(IDQL"bQ!(&E1(IDQL" +7D$(&E1(ԬD$(&E1(QD$(&"bQD$pCDQL"bQDn(IDQL"bQ E1(IDQL"bQ!(&E1(IDQL" +7D$(&E1(IDᆈD$(&E1(QD$(&QD$(&"bQD$pCDQL"bQDn(IDQL"bQ E1(IDQL"bQ!(&E1(IDQL" +7D$(&E1(IDᆈD$(&E1(QD$(&"bQD$P"bQD$pCDQL"bQDn(IDQL"bQ E1(IDQL"bQ!(&E1(IDQL" +7D$(&E1(IDᆈD$(&E1(QD$(&"bQD$pCDQL"bQDjVDQL"bQDn(IDQL"bQ E1(IDQL"bQ!(QD$pCDQ#PL1FD{oDq\}~-c#kp9g!w?GHS;M/w9oo5"+fvwx7Sy͟ʋ^_ >yB  %FVyB  %FVyB  %Fvy  7jVy  7jVy  7jVy  7jVy  7jVy  7jVy  7jVy  7jVy  7jVy  7fvy  7jVy  7jVy  7jVy  7jVy  7jVy  7jVy  7jVy  7jVy  7jVy  7fvy  7jVy  7jVy  7jVy  7jVy  7jVy  7jVy  7jVy  7jVy  7jVy  3pCy  5pCy  5pCy  5pCy  5pCy  5pCy  5pCy  5pCy  5pCy  5pCy  3pCy  5pCy  5pCy  5pCy  5pCy  5pCy  5pCy  5pCy  5pCy  5pCy  3pCy  5pCy  5pCy  5pCy  5pCy  5pCy  5pCy  5pCy  5pCy  5pCy  3pCy  5pCy  5pCy  5pCy  5pCyay}࿭yʋ4Ey._s,~ήx[ˋcў؈}*/Syq{_7{Y}J(/P^(0 %J(/P^(0 %J(/P^(0 7n(/P^P 7n(/P^P 7n(/P^P 7n(/P^P 7n(/P^P 7n(/P^P 7n(/P^P 7n(/P^P 7n(/P^P 7n(/P^0 7n(/P^P 7n(/P^P 7n(/P^P 7n(/P^P 7n(/P^P 7n(/P^P 7n(/P^P 7n(/P^P 7n(/P^P 7n(/P^0 7n(/P^P 7n(/P^P 7n(/P^P 7n(/P^P 7n(/P^P 7n(/P^PspCy  5pCy  5pCy  5pCy 兙]^pCy 党U^pCy 党U^pCy 党U^pCy 党U^pCy 党U^pCy 党U^pCy 党U^pCy 党U^pCy 党U^pCy 兙]^pCy 党U^pCy 党U^pCy 党U^pCy 党U^pCy 党U^pCy 党U^pCy 党U^pCy 党U^pCy 党U^pCy 兙]^pCy 党U^pCy 党U^pCy 党U^pCy 党U^pCy 党U^pCy 党U^pCy 党U^pCy 党U^pCy 党U^pCy 兙]^pCy 党U^pCy 党U^pCy 党U^pCy 党U^0˼>Fy{s9/+/7?./Q^|}ZX|*,Sa񸮛ۿ>Wo + %J(,PXY + %J(,PXY + %J(,PXم + 7n(,PXY + 7n(,PXY + 7n(,PXY + 7n(,PXY + 7n(,PXY + 7n(,PXQXpCa UXpCa UXpCa UXpCa ]XpCa UXpCa UXpCa UXpCa UXpCa UXpCa  + 7n(,PXY + 7n(,PXY + 7n(,PXY + 7n(,PXم + 7n(,PXY + 7n(,PXY + 7n(,PXY + 7n(,PXY + 7n(,PXY + 7n(,PXQXpCa UXpCa UXpCa UXpCa.,PXpCaB*,PXpCaB*,PXpCaB*,PXpCaB*,PXpCaB*,PXpCaB*,PXpCaB*,PXpCaB*,PXpCaB*,PXpCa.,PXpCaB*,PXpCaB*,PXpCaB*,PXpCaB*,PXpCaB*,PXpCaB*,PXpCaB*,PXpCaB*,PXpCaB*,PXpCa.,PXpCaB*,PXpCaB*,PXpCaB*,PXpCaB*,PXpCaB*,PXpCaB*,PXpCaB*,PXpCaB*,PXpCaB*,PXpCa.,PXpCaB*,PXpCaB*,PXpCaB*,PXpCaB*,PXe.AF|= +y q{WX o~]XtOu/~mgy>_]N/yznw?y?_ŤpCQLba +dn0IQLJbb͊1JPrc4 d4&D (,t %h(eFCI3 +h(qFC3JP4JPh4H *i4N (T %h(FC5 +h(FC)6JP6JPm4p ʍn4v po4 %(D h(GC8 +"h(%GCI9JPb9JPzt4 t4T %( h(iGCi; +"h(uGC;JP(<JPy4ʣ 2y4У %(֣ h(GC=࣡ %h(GC> +h(GC>JPʏH?JP⏆R4 4$ )T %i(HC A +i()HCiAJ PjAJPR4$ 4*d! )0! % i(mHCC +i(yHCCJ h" %i(HCDJ%RHCDJ(PJD+PbR4\"4b$# i(HAT# %i(HC GJ9RHCiGJ* Pi(JA4* %Ri(JCTJRJC)UJPZDPrҫ4`+4fD+ Zi(يխ4p+ %]i(JA+ ^i(JCWJRKCIXJP"RDP:4,Ѳ4/3 2N5UQϺ2gy,erq_5Wy=9(r___Mtʏկ|?Evr~>dۿŽ>gq2p\uz?}ɟ鸄yǯ>;~z^5qh endstream endobj 8 0 obj @@ -883,7 +889,7 @@ endobj /BS << /W 0 >> -/Dest (.:ameliorations-futures) +/Dest (.:optimisation-du-programme) >> endobj 40 0 obj @@ -894,7 +900,7 @@ endobj /BS << /W 0 >> -/Dest (.:ameliorations-futures) +/Dest (.:optimisation-du-programme) >> endobj 41 0 obj @@ -905,7 +911,7 @@ endobj /BS << /W 0 >> -/Dest (.:ameliorations-futures) +/Dest (.:optimisation-du-programme) >> endobj 42 0 obj @@ -916,7 +922,7 @@ endobj /BS << /W 0 >> -/Dest (.:conclusion) +/Dest (.:ethique-du-projet) >> endobj 43 0 obj @@ -927,7 +933,7 @@ endobj /BS << /W 0 >> -/Dest (.:conclusion) +/Dest (.:ethique-du-projet) >> endobj 44 0 obj @@ -938,29 +944,29 @@ endobj /BS << /W 0 >> -/Dest (.:conclusion) +/Dest (.:ethique-du-projet) >> endobj 45 0 obj << /Type /Annot /Subtype /Link -/Rect [ 45.466457 500.675622 557.809134 482.565622 ] +/Rect [ 53.466457 500.675622 557.809134 482.565622 ] /BS << /W 0 >> -/Dest (CahierDesCharges/:cahier-des-charges) +/Dest (.:ameliorations-futures) >> endobj 46 0 obj << /Type /Annot /Subtype /Link -/Rect [ 45.466457 496.115622 59.641994 483.315622 ] +/Rect [ 53.466457 496.115622 77.821193 483.315622 ] /BS << /W 0 >> -/Dest (CahierDesCharges/:cahier-des-charges) +/Dest (.:ameliorations-futures) >> endobj 47 0 obj @@ -971,7 +977,7 @@ endobj /BS << /W 0 >> -/Dest (CahierDesCharges/:cahier-des-charges) +/Dest (.:ameliorations-futures) >> endobj 48 0 obj @@ -982,18 +988,18 @@ endobj /BS << /W 0 >> -/Dest (CahierDesCharges/:contexte) +/Dest (.:conclusion) >> endobj 49 0 obj << /Type /Annot /Subtype /Link -/Rect [ 53.466457 478.005622 72.731593 465.205622 ] +/Rect [ 53.466457 478.005622 77.821193 465.205622 ] /BS << /W 0 >> -/Dest (CahierDesCharges/:contexte) +/Dest (.:conclusion) >> endobj 50 0 obj @@ -1004,29 +1010,29 @@ endobj /BS << /W 0 >> -/Dest (CahierDesCharges/:contexte) +/Dest (.:conclusion) >> endobj 51 0 obj << /Type /Annot /Subtype /Link -/Rect [ 53.466457 464.455622 557.809134 446.345622 ] +/Rect [ 45.466457 464.455622 557.809134 446.345622 ] /BS << /W 0 >> -/Dest (CahierDesCharges/:projet) +/Dest (CahierDesCharges/:cahier-des-charges) >> endobj 52 0 obj << /Type /Annot /Subtype /Link -/Rect [ 53.466457 459.895622 72.731593 447.095622 ] +/Rect [ 45.466457 459.895622 59.641994 447.095622 ] /BS << /W 0 >> -/Dest (CahierDesCharges/:projet) +/Dest (CahierDesCharges/:cahier-des-charges) >> endobj 53 0 obj @@ -1037,7 +1043,7 @@ endobj /BS << /W 0 >> -/Dest (CahierDesCharges/:projet) +/Dest (CahierDesCharges/:cahier-des-charges) >> endobj 54 0 obj @@ -1048,7 +1054,7 @@ endobj /BS << /W 0 >> -/Dest (CahierDesCharges/:realisation) +/Dest (CahierDesCharges/:contexte) >> endobj 55 0 obj @@ -1059,7 +1065,7 @@ endobj /BS << /W 0 >> -/Dest (CahierDesCharges/:realisation) +/Dest (CahierDesCharges/:contexte) >> endobj 56 0 obj @@ -1070,7 +1076,7 @@ endobj /BS << /W 0 >> -/Dest (CahierDesCharges/:realisation) +/Dest (CahierDesCharges/:contexte) >> endobj 57 0 obj @@ -1081,7 +1087,7 @@ endobj /BS << /W 0 >> -/Dest (CahierDesCharges/:cas-dutilisation) +/Dest (CahierDesCharges/:projet) >> endobj 58 0 obj @@ -1092,7 +1098,7 @@ endobj /BS << /W 0 >> -/Dest (CahierDesCharges/:cas-dutilisation) +/Dest (CahierDesCharges/:projet) >> endobj 59 0 obj @@ -1103,7 +1109,7 @@ endobj /BS << /W 0 >> -/Dest (CahierDesCharges/:cas-dutilisation) +/Dest (CahierDesCharges/:projet) >> endobj 60 0 obj @@ -1114,7 +1120,7 @@ endobj /BS << /W 0 >> -/Dest (CahierDesCharges/:difficultes-techniques) +/Dest (CahierDesCharges/:realisation) >> endobj 61 0 obj @@ -1125,7 +1131,7 @@ endobj /BS << /W 0 >> -/Dest (CahierDesCharges/:difficultes-techniques) +/Dest (CahierDesCharges/:realisation) >> endobj 62 0 obj @@ -1136,29 +1142,29 @@ endobj /BS << /W 0 >> -/Dest (CahierDesCharges/:difficultes-techniques) +/Dest (CahierDesCharges/:realisation) >> endobj 63 0 obj << /Type /Annot /Subtype /Link -/Rect [ 45.466457 392.015622 557.809134 373.905622 ] +/Rect [ 53.466457 392.015622 557.809134 373.905622 ] /BS << /W 0 >> -/Dest (jdb/:journal-de-bord) +/Dest (CahierDesCharges/:cas-dutilisation) >> endobj 64 0 obj << /Type /Annot /Subtype /Link -/Rect [ 45.466457 387.455622 59.641994 374.655622 ] +/Rect [ 53.466457 387.455622 72.731593 374.655622 ] /BS << /W 0 >> -/Dest (jdb/:journal-de-bord) +/Dest (CahierDesCharges/:cas-dutilisation) >> endobj 65 0 obj @@ -1169,7 +1175,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:journal-de-bord) +/Dest (CahierDesCharges/:cas-dutilisation) >> endobj 66 0 obj @@ -1180,7 +1186,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:mercredi-29-mars-2023) +/Dest (CahierDesCharges/:difficultes-techniques) >> endobj 67 0 obj @@ -1191,7 +1197,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:mercredi-29-mars-2023) +/Dest (CahierDesCharges/:difficultes-techniques) >> endobj 68 0 obj @@ -1202,29 +1208,29 @@ endobj /BS << /W 0 >> -/Dest (jdb/:mercredi-29-mars-2023) +/Dest (CahierDesCharges/:difficultes-techniques) >> endobj 69 0 obj << /Type /Annot /Subtype /Link -/Rect [ 53.466457 355.795622 557.809134 337.685622 ] +/Rect [ 45.466457 355.795622 557.809134 337.685622 ] /BS << /W 0 >> -/Dest (jdb/:jeudi-30-mars-2023) +/Dest (jdb/:journal-de-bord) >> endobj 70 0 obj << /Type /Annot /Subtype /Link -/Rect [ 53.466457 351.235622 72.731593 338.435622 ] +/Rect [ 45.466457 351.235622 59.641994 338.435622 ] /BS << /W 0 >> -/Dest (jdb/:jeudi-30-mars-2023) +/Dest (jdb/:journal-de-bord) >> endobj 71 0 obj @@ -1235,7 +1241,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:jeudi-30-mars-2023) +/Dest (jdb/:journal-de-bord) >> endobj 72 0 obj @@ -1246,7 +1252,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:vendredi-31032023) +/Dest (jdb/:mercredi-29-mars-2023) >> endobj 73 0 obj @@ -1257,7 +1263,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:vendredi-31032023) +/Dest (jdb/:mercredi-29-mars-2023) >> endobj 74 0 obj @@ -1268,7 +1274,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:vendredi-31032023) +/Dest (jdb/:mercredi-29-mars-2023) >> endobj 75 0 obj @@ -1279,7 +1285,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:lundi-3-avril) +/Dest (jdb/:jeudi-30-mars-2023) >> endobj 76 0 obj @@ -1290,7 +1296,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:lundi-3-avril) +/Dest (jdb/:jeudi-30-mars-2023) >> endobj 77 0 obj @@ -1301,7 +1307,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:lundi-3-avril) +/Dest (jdb/:jeudi-30-mars-2023) >> endobj 78 0 obj @@ -1312,7 +1318,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:mardi-4-avril) +/Dest (jdb/:vendredi-31032023) >> endobj 79 0 obj @@ -1323,7 +1329,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:mardi-4-avril) +/Dest (jdb/:vendredi-31032023) >> endobj 80 0 obj @@ -1334,7 +1340,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:mardi-4-avril) +/Dest (jdb/:vendredi-31032023) >> endobj 81 0 obj @@ -1345,7 +1351,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:mercredi-5-avril) +/Dest (jdb/:lundi-3-avril) >> endobj 82 0 obj @@ -1356,7 +1362,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:mercredi-5-avril) +/Dest (jdb/:lundi-3-avril) >> endobj 83 0 obj @@ -1367,7 +1373,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:mercredi-5-avril) +/Dest (jdb/:lundi-3-avril) >> endobj 84 0 obj @@ -1378,7 +1384,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:jeudi-6-avril) +/Dest (jdb/:mardi-4-avril) >> endobj 85 0 obj @@ -1389,7 +1395,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:jeudi-6-avril) +/Dest (jdb/:mardi-4-avril) >> endobj 86 0 obj @@ -1400,7 +1406,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:jeudi-6-avril) +/Dest (jdb/:mardi-4-avril) >> endobj 87 0 obj @@ -1411,7 +1417,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:vendredi-6-avril-2023) +/Dest (jdb/:mercredi-5-avril) >> endobj 88 0 obj @@ -1422,7 +1428,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:vendredi-6-avril-2023) +/Dest (jdb/:mercredi-5-avril) >> endobj 89 0 obj @@ -1433,7 +1439,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:vendredi-6-avril-2023) +/Dest (jdb/:mercredi-5-avril) >> endobj 90 0 obj @@ -1444,7 +1450,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:vacances) +/Dest (jdb/:jeudi-6-avril) >> endobj 91 0 obj @@ -1455,7 +1461,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:vacances) +/Dest (jdb/:jeudi-6-avril) >> endobj 92 0 obj @@ -1466,7 +1472,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:vacances) +/Dest (jdb/:jeudi-6-avril) >> endobj 93 0 obj @@ -1477,29 +1483,29 @@ endobj /BS << /W 0 >> -/Dest (jdb/:lundi-24-avril-2023) +/Dest (jdb/:vendredi-6-avril-2023) >> endobj 94 0 obj << /Type /Annot /Subtype /Link -/Rect [ 53.466457 206.355622 77.821193 193.555622 ] +/Rect [ 53.466457 206.355622 72.731593 193.555622 ] /BS << /W 0 >> -/Dest (jdb/:lundi-24-avril-2023) +/Dest (jdb/:vendredi-6-avril-2023) >> endobj 95 0 obj << /Type /Annot /Subtype /Link -/Rect [ 542.540335 206.355622 557.809134 193.555622 ] +/Rect [ 547.629935 206.355622 557.809134 193.555622 ] /BS << /W 0 >> -/Dest (jdb/:lundi-24-avril-2023) +/Dest (jdb/:vendredi-6-avril-2023) >> endobj 96 0 obj @@ -1510,29 +1516,29 @@ endobj /BS << /W 0 >> -/Dest (jdb/:mardi-25-avril-2023) +/Dest (jdb/:vacances) >> endobj 97 0 obj << /Type /Annot /Subtype /Link -/Rect [ 53.466457 188.245622 77.821193 175.445622 ] +/Rect [ 53.466457 188.245622 72.731593 175.445622 ] /BS << /W 0 >> -/Dest (jdb/:mardi-25-avril-2023) +/Dest (jdb/:vacances) >> endobj 98 0 obj << /Type /Annot /Subtype /Link -/Rect [ 542.540335 188.245622 557.809134 175.445622 ] +/Rect [ 547.629935 188.245622 557.809134 175.445622 ] /BS << /W 0 >> -/Dest (jdb/:mardi-25-avril-2023) +/Dest (jdb/:vacances) >> endobj 99 0 obj @@ -1543,7 +1549,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:26-avril-2023) +/Dest (jdb/:lundi-24-avril-2023) >> endobj 100 0 obj @@ -1554,7 +1560,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:26-avril-2023) +/Dest (jdb/:lundi-24-avril-2023) >> endobj 101 0 obj @@ -1565,7 +1571,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:26-avril-2023) +/Dest (jdb/:lundi-24-avril-2023) >> endobj 102 0 obj @@ -1576,7 +1582,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:jeudi-27-avril-2023) +/Dest (jdb/:mardi-25-avril-2023) >> endobj 103 0 obj @@ -1587,7 +1593,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:jeudi-27-avril-2023) +/Dest (jdb/:mardi-25-avril-2023) >> endobj 104 0 obj @@ -1598,7 +1604,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:jeudi-27-avril-2023) +/Dest (jdb/:mardi-25-avril-2023) >> endobj 105 0 obj @@ -1609,7 +1615,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:vendredi-28-avril-2023) +/Dest (jdb/:26-avril-2023) >> endobj 106 0 obj @@ -1620,7 +1626,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:vendredi-28-avril-2023) +/Dest (jdb/:26-avril-2023) >> endobj 107 0 obj @@ -1631,7 +1637,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:vendredi-28-avril-2023) +/Dest (jdb/:26-avril-2023) >> endobj 108 0 obj @@ -1642,7 +1648,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:lundi-1-mai-2023) +/Dest (jdb/:jeudi-27-avril-2023) >> endobj 109 0 obj @@ -1653,7 +1659,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:lundi-1-mai-2023) +/Dest (jdb/:jeudi-27-avril-2023) >> endobj 110 0 obj @@ -1664,7 +1670,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:lundi-1-mai-2023) +/Dest (jdb/:jeudi-27-avril-2023) >> endobj 111 0 obj @@ -1675,7 +1681,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:mardi-2-mai-2023) +/Dest (jdb/:vendredi-28-avril-2023) >> endobj 112 0 obj @@ -1686,7 +1692,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:mardi-2-mai-2023) +/Dest (jdb/:vendredi-28-avril-2023) >> endobj 113 0 obj @@ -1697,408 +1703,377 @@ endobj /BS << /W 0 >> -/Dest (jdb/:mardi-2-mai-2023) +/Dest (jdb/:vendredi-28-avril-2023) >> endobj 114 0 obj << /Filter /FlateDecode -/Length 31321 +/Length 33607 >> stream -xA$Wg=?LPi1F@ #@ТM3̼F]͢Ǐ޳u_ﯷm{;m?|?qc?v?ѷo5?^omϟxk>,jԠn>ӹ=u9k|ngj9mcymE3un0W m{K WU-$0{Uݏ3_-~۹ԯ~FsՈ} ùjyԘs1~kkuqx 뵿xjHyԘlo~ܷd2ܜo{+~i<۽J48gO]osxj&;xZ/ЕSW}߯-g15Sc>sԘYnioԘkr ZLy4zxjמS|Կ^25y`?nst}~<ﵶLf+zQ|2ǹs=E}^`jU-]koNƼb$S_دzdju ?`j|Hy|ܞi<5ִS3j2vq<5i]i<5>kxj|_=gi<5 gmni<5=]ϵ+;j|Ն']k{=Vo5VH{WVUPO̓y=VU++v<^es]J{ZyLQ3fO̓#k?yڎS]Wo5^{-Ixjzo5^=M{Y4{K{bn\Uv1e[>[}]xj9RfkO`_v^^ejvJט={vq~1ׄ\tvטƼOyԘ5c"VԘkJ9xoHzُ߷s͸o5H{}1jƳטdjWxVBq<2q+smi}{~ejsԘ-]\ǯ}y֘krN`g.Ti?k¦{\K1fYci_ Fs֘ǙYc sj̵K{Uc>׏e10k.sl]tI{16G^i<׳H{z=ꍘG̓wWQG̓;yQt}>j|;[B8ϴ?jJZnQ`>VosK`ke-k*-z{&}IG̓z{G̓~=~5nK{myǯ ekO`տ+{>5֛H{ڢ~GeW o۹?yp;_G5nuiI_==أ;iXzܯty[tY`f=Rt}~<ۙu+SxjGri<5ֺLmV[גL]xjܟ |.5Ziy3]zۯԘϚktOg̓:,z6i_ Q4kH{g̓8=ؚԔScޮGy߄iy뭑S`͗ǯeV3݃[sW?keW[[z;y~Y`x=Y4[{{=Y`ty_˕g̓u=~m\W>o~=Y`:>y~?k*XڢjER^Vn[nv7}˷ޏ>?b|n3u6?}<[ (/d,!} %J(/B  %J(/B  %J(/  7n(/Ԍ  7n(/Ԍ  7n(/Ԍ  7n(/n(/P^pCyfn(/P^pCyfn(/P^pCyfn(/P^pCyfn(/P^pCyfn(/P^pCyafn(/P^pCyfn(/P^pCyfn(/P^pCyfn(/P^pCyfpCy  5pCy  5pCy  5pCy  5pCy  5pCy  3pCy  5pCy  5pCy  5pCy  5; 7n(/P^ 7Gn(/P^ 7 7n(/Ԍ 兛 7n(/Ԍ 兛 7n(/Ԍ 兛 7ffy  7jFy  7jFy  7jFy  7jFy  7jFy  7jFy  7jFy  7jFy  7jFy  7ffy  7jFy  7jFy  7jFy  7jFy  7jFy  7jFy  7jFy  7jFy  7jFy  7ffy  7jFy  7jFy  7jFy  7jFy  7jFy  7jFy  7jFy  7jFy  7jFy  7ffy  7jFy  7jFy  7jFy  7jFy̯+P^6ʋ}>kY?X^|)/><~Si⺯CJ PZԥ糌?gqJ(-PZ(02J %J(-PZ(02J %J(-PZ(02K 7n(-PZP3J 7n(-PZP3J 7n(-PZP3J 7n(-PZP3J 7n(-PZP3J 7n(-PZP3J 7n(-PZP3J 7n(-PZP3J 7n(-PZP3J 7n(-PZ03K 7n(-PZP3J 7n(-PZP3J 7n(-PZP3J 7n(-PZP3J 7n(-PZP3J 7n(-PZP3J 7n(-PZP3J 7n(-PZP3J 7n(-PZP3J 7n(-PZ03K 7n(-PZP3J 7n(-PZP3J 7n(-PZP3J 7n(-PZP3J 7n(-PZP3J 7n.-PZP3J 7noJ 7jFiѥJ 7jFiѥJ 7jFiѥJ 3pCi J 5pCi J 5pCi J 5pCi J 5pCi J 5pCi J 5pCi J 5pCi J 5pCi J 5J 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZ(-PZpCi,-PZpCiB(-PZpCiB(-PZpCiB(-PZpCiB(-PZpCiB(-PZpCiB(-PZpCiB(-PZpCiB(-PZpCiBEi J 7ffi J 7jFi J 7jFi J 7jFi J 7jFi̯'PZ6JK_QZ|(-7g\|/)ϵ/b}+Z} `me<fO{~Ajd_dtɚ*ݜkۚd 歓5U{'kGUqakQPGZ:^%A- "8'kXY@p_kd U@p_Jk1܅WkE_W=7ϵXz<Ðk1PG}up_Z:b`x!k1=jHfnb` oZ l0L0k[-ߒYcVL=b{_ܮE1ۖ^mX #sk1?3s-Ә5s}0)5Lűzkn<_WkO2:}yvx-kҘ<=zqKfDz-_ϴ^<|]iyymiyy: -niypeNŚۙ֫ۚ=W5#W5WZnk<-y̓g]k~b̓綥s{vzu[45֫ۚ֫ۚcOm̓CKm̓ -Yc~ƛۚ3M֫+9WGZGʜis+Yw*zu#e5 GԖ֫+)sG>>\&\u̓|^u̓[G -2WWmI6iN '}چ>mC m86iN '}چqF0}F0}F0}F0}چNOOOOpiiiiin8m#>m#>m#>m#>m mӧmӧmӧmӧmᴍ```` 7LLLL66666pF0}F0}F0}F0}چNOOOOP3NOOOOpiiiiin8m#>m#>m#>m#>m mӧmӧmӧmӧmᴍ```` 7LLLL66666pF0}F0}F0}F0}چNOOOOpiiiiijiiiiin8m#>m#>m#>m#>m mӧmӧmӧmӧmᴍ```` 7LLLL66666pF0}F0}F0}F0}چNOOOOpiiiiin8m#>m#>m#>mC8m#>m#>m#>m#>m mӧmӧmӧmӧmᴍ```` 7LLLL66666pF0}F0}F0}F0}چNOOOOpiiiiin8m#>m#>m#>m#>mէmӧm8m#>m#>mC8m#>m#I666pF0}F0666pF0}F0666pF0}F066y8m mӧmӧmӧmqچNOOO 7LLLin8m#>m#>m#>m#׏6pF0}F0}F0}F0}چNOOOOP3NOOOOpiiiiin8m#>m#>m#>m#>m mӧmӧmӧmӧmᴍ```` 7LLLL66666pF0}F0}F0}F0}چNOOOOpiiiiijiiiiin8m#>m#>m#>m#>m mӧmӧmӧmӧmᴍ```` 7L9O۶>>xֲ>!׾`b}_Euև_mFm}?xc涝=Ͽ|[77ˇ肇?Dkv糌?}tGJ.](!02 %DJ.](!02 %DJ.](!02 7Dn.]!P3 7Dn.]!P3 7Dn.]!P3 7Dn.]!P3 7Dn.]!P3 7Dn.]!P3 7Dn.]!P3 7Dn.]!P3 7Dn.]!P3 7Dn.]!03 7Dn.]!P3 7Dn.]!P3 7Dn.]!P3 7Dn.]!P3 7Dn.]!P3 7Dn.]!P3 7Dn.]!P3 7Dn.]!P3 7Dn.]!P3 7Dn.]!03 7Dn.]!P3 7Dn.]!P3 7Dn.]!P3 7Dn.]!P3 7Dn.]!P3 7Dn.]!P3 7Dn.]!P3 7Dn.]!P3 7Dn.]!P3 7Dn.]х 7Dn.]х 7Dn.]х 7Dn.]х 7Dn.]х 7Dn.]х 7Dn.]х 7Dn.]х 7Dn.]х 7Dn.]х 7Dn.]х 7Dn.]х 7Dn.]х 7Dn.]х 7Dn.]х 7Dn.]х 7Dn.]х 7Dn.]х 7Dn.]х 7Dn.]х 7Dn.]х 7Dn.]х 7Dn.]х 7Dn.]хGGn.]!P3 7.]!pCtfDn]!pCtB͈.<:pCt х]ytt х 5#pCt х 5#pCt х 33pCt х 5#pCt х 5#pCt х 5#pCt х 5#pCtaWG_.~i˹_.F_6?#G=8+i1xY_v7eޟkaF=~eƫ|/i>PB2C eQf(PB2C eQf(PB2C eYfpC2 eQfpC2 eQfpC2 eQfpC2 eQfpC2 eQfpC2 eQfpC2 eQfpC2 eQfpC2 eQfpC2 eYfpC2 eQfpC2 eQfpC2 eQfpC2 eQfpC2 eQfpC2 eQfpC2 eQfpC2 eQfpC2 eQfpC2 eYfpC2 eQfpC2 eQfpC2 eQfpC2 eQfpC2 eQfpC2 eQfpC2 eQfpC2 eQfpC2 eQfpC2,3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2,3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2,3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3Pfp2 eQfpCe 5pC2ͳ 7ff2 eoe 5pC2ͳ 7jF2 e 7jF2 e 7jF2̯3Pf6ʌj2?Xf|(3lض^<65Ʒ'6÷|INp5j ']c8INPBk ']c8IJFLtLt#1#1 5F0]c5F0]c5j`k`k 7tLtLn1#1#1Pc5F0]c5F0]ck`k`pCLtLt#1#1Cͨ1#1#1Pc5F0]c5F0]ck`k`pCLtLt#1#1 5F0]c5F0]c5j`k`k 7tLtLn1#1#1Pc5F0]c5F0]c5F0]c5F0]c5j`k`k 7tLtLn1#1#1Pc5F0]c5F0]ck`k`pCLtLt#1#1 5F0]c5F0]c5j`k`P3j`k`k 71#1#1Pc5F0]c5F0]ck`k`pCLtLt#1#1 5F0]c5F0]c5j`k`k 7tLtLn1#1#1Ԍ#1#1ͽk`k`k 7tLtLn1#1#1Pc5F0]c5F0]ck`k`pCLtLt#1#1 5F0]c5F0]c5j`k`k 5k`k`ps#1#1 5F0]c5F0]c5j`k`k 7tLtLn1#1#1Pc5F0]c5F0]ck`k`pCLtLt#1#1Cͨ1#1#1ܻk`k`pCLtLt#1#1 5F0]c}jV~ =ϳ[c; 1m~g1>>{q}ck6Y[mq0ze7;s,Y+ =ܕ* %TJ,B * %TJ,B * %TJ, * 7Tn,Ԍ * 7Tn,Ԍ * 7Tn,Ԍ * 7Tn,Ԍ * 7Tn,Ԍ * 7Tn,Ԍ * 7Tn,Ԍ * 7Tn,Ԍ * 7Tn,Ԍ * 7Tn, * 7Tn,Ԍ * 7Tn,Ԍ * 7Tn,Ԍ * 7Tn,Ԍ * 7Tn,Ԍ * 7Tn,Ԍ * 7Tn,Ԍ * 7Tn,Ԍ * 7Tn,Ԍ * 7Tn, * 7Tn,Ԍ * 7Tn,Ԍ * 7Tn,Ԍ * 7Tn,Ԍ * 7Tn,Ԍ * 7Tn,Ԍ * 7Tn,Ԍ * 7Tn,Ԍ * 7Tn,Ԍ * 7Tffe * 7TjTn,PYpCefTn,PYpCefTn,PYpCefTn,PYpCefTn,PYpCefTn,PYpCefTn,PYpCefTn,PYpCefTn,PYpCeafVn,PYpCefTn,PYpCefTn,PYpCefTn,PYpCefTn,PYpCefTn,PYpCefTn,PYpCefTn,PYpCefTn,PYpCefTn,PYpCeafVn,PYpCefTn,PYpCefTn,PYpCefTn,PYpCefTn,PYpCefTn,PYpCefTn,PYpCefTn,PYpCefTn,PYpCefTn,PYpCeafVn,PYpCefTn,PYpCefTn,PYpCefTn,PYpCefTn,l>+ ZeQ/_,?XY|,^lGϸX_J/vSY|[c -%J)S1b -%J)S1b -%J)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)S1b -7n)̘ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)̘ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)̘ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)̘ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)Ԍ 1b -7n)Ԍ 1_AЧ|!m5>j X[/?S|)S|xo3)Soև?m<_PBQB EQT(PBQB EQT(PBQB E(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢ,*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢ,*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQafn(*PTpCQfn(*PTpCQfn(*PTpCQfn(*PTpCQfn(*PTpCQfn(*PTpCQfn(*PTpCQfn(*PTpCQfn(*PTpCQfn(*PTpCQafn(*PTpCQfn(*PTpCQfn(*PTpCQfn(*PTpCQfn(*PTpCQfn(*PTpCQfn(*PTpCQfn(*PTpCQfn(*PTpCQfn(*PTpCQafn(*PTpCQfn(*PTpCQfn(*PTpCQfn(*PTpCQfn(*PTpCQfn(*PTpCQfn(*PTpCQfn(*PTpCQfn(*PTpCQfn(*PTpCQafn(*PTpCQfn(*PTpCQfn(*PTpCQfn(*PTpCQfn(*|<ׇؿPPoחXw{2?><糌?9>a$N:pIN*T8I'N:pI -'T8I'N:P2`:`: -7$tRL'tRL'nH*"N*"N*ܐTIE0TIE0T!`:`:pCRL'tRL'tRᆤ"N*"N* IE0TIE0TI`:`: -7$tRL'tRL'jFRL'tRL'tRᆤ"N*"N* IE0TIE0TI`:`: -7$tRL'tRL'nH*"N*"N*ܐTIE0TIE0T!`:`:pCRL'tRL'tRᆤ"N*"N*BH*"N*"GRᆤ"N*"N*~$nH*"N*"GRᆤ"N*"N*~$nH*"N*"GRᆤ"GRL'tRL'nH*~$tRL'tRᆤ"GRL'tRL'nH*~$tRL'tRᆤ""N*BH*"N*"N*ܐTIE0TIE0T!`:`:pCRL'tRL'tRᆤ"N*"N* IE0TIE0TI`:`: -7$tRL'tRL'nH*"N*"N*ܐTIE0TIE0TIE0TIE0TI`:`: -7$tRL'tRL'nH*"N*"N*ܐTIE0TIE0T!`:`:pCRL'tRL'tRᆤ"N*"N* IE0TIE0TI`:`: -5#`:`:pCRL'tRL'tRᆤ"N*"N* IE0TIE0TI`:`: -7$tRL'tRL'nH*"N*"N*ܐTIE0TIE0T!`:`:pCRL'tRL'tRf$tRL'tRL'nH*"N*"N*ܐTIE0TIE0T!`:`:pCRL'j>'iT\W}X_J*/~TT|x~8[NŊQxoZ>T;PB~B _(!PB~B _(!PB~B _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~/ܐ_!pC~B/ܐ_!pC~B/ܐ_!pC~B/ܐ_!pC~B/ܐ_!pC~B/ܐ_!pC~B/ܐ_!pC~B/ܐ_!pC~B/ܐ_!pC~B/<:pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ _!pC~ 5#pC~· _!pC~nwF( -Ѩ^4׹a>Dzq8 o~ ›S7 m_x~)6/_x~ B/_x~M o/_xЦ7 o/_hS7 o/)~ 7 o~ 7 k_x~ 6/_x~ B/_x~M o/_xЦ7 k>LZn?8.y|;ź_~_x3//V91?t\xz3M0jWw5kwS-{ `xI& OB0< $Ó M `xI'!MhB0F1 o  MhB0F c4! MhB0`&c4! M7ф`&c4! 1ф`&c4!@0F1ф`&hB0F1ф`xMhB0F1 o  MhB0FMф`&c4! 1ф`&c4!@0F1ф`&hB0F1ф`xMhB0F1 o  MhB0F c4! MhB0`&c4! M7ф`&c4! 1ф`&c4!MhB0F1 o  MhB0F c4! MhB0`&c4! M7ф`&c4! 1ф`&c4!@0F1ф`&hB0F1ф`xMhB0F1 o  MhB0)1ф`&c4!@0F1ф`&hB0F1ф`xMhB0F1 o  MhB0F c4! MhB0`&c4! M7ф`&c4! 1ф`&c4!@0F1ф`&C" MhB0F c4! MhB0`&c4! M7ф`&c4! 1ф`&c4!@0F1ф`&hB0F1ф`xMhB0F1 o  MhB0F c4! MhB09\hB0F1ф`xMhB0F1 o  MhB0F c4! MhB0`&c4! M7ф`&c4! 1ф`&c4!@0F1ф`&hB0F1ф`xMhB0F1 m`&c4! M7ф`&c4! 1ф`&c4!@0F1ф`&hB00qA@`2yMыU"+U8eRUQ*4AUhT& -MP*4AUXRBT& -MP*,iU T7 -oPڔU T7 -mJUxU T6*AUxU BRޠ*AUxUM -oPޠ*AUxЦT7 -oPޠ*AUhST7 -oPޠ*)U T7 -oPڔU T7 -kZUxU T6*AUxU BRޠ*AUxUM -oPޠ*AUxЦT7 -oPޠ*AUhST7 -oPޠ*)U T7 -oPڔU T7 -mJUxU T6*AUxU šVޠ*AUxUM -oPޠ*AUxЦT7 -oPޠ*AUhST7 -oPޠ*)U T7 -oPڔU T7 -mJUxU T6*AUxU BRޠ*AUxUM -oPޠ*AUXӪT7 -oPޠ*)U T7 -oPڔU T7 -mJUxU T6*AUxU BRޠ*AUxUM -oPޠ*AUxЦT7 -oPޠ*AUhST7 -oPޠ*)U T7 -oPִU T7 -mJUxU T6*AUxU BRޠ*AUxUM -oPޠ*AUxЦT7 -oPޠ*AUhST7 -oPޠ*)U T7 -oPڔU T7 -mJUxU T5*AUxU BRޠ*AUxUM -oPޠ*AUxЦT7 -oPޠ*AUhST7 -oPޠ*)U T7 -oPڔU T7 -mJUxU T6*AUxU BRޠ*AUxUaM -oPޠ*AUxЦT7 -oPޠ*AUhST7*AUxЦT7 -o>EUxUM -oP| ZUHT(8qv{֛iUmUe⊧d7+sR _ ý|1U BT& -KJUhU BT%*4AUhU ’Vޠ*AUxUM -oPޠ*AUxЦT7 -oPޠ*AUhST7 -oPޠ*)U T7 -oPڔU T7 -mJUxU T6*AUxU BRޠ*AUxUM -oPޠ*AUxU7 -oPޠ*AUhST7 -oPޠ*)U T7 -oPڔU T7 -mJUxU T6*AUxU BRޠ*AUxUM -oPޠ*AUxЦT7 -oPޠ*AUhST7 -oPޠ*iU T7 -oPڔU T7 -mJUxU T6*AUxU BRޠ*AUxUM -oPޠ*AUxЦT7 -oPޠ*AUhST7 -oPޠ*)U T7 -oPڔU T5*AUxU BRޠ*AUxUM -oPޠ*AUxЦT7 -oPޠ*AUhST7 -oPޠ*)U T7 -oPڔU T7 -mJUxU T6*AUxU BRޠ*AUxUaM -oPޠ*AUxЦT7 -oPޠ*AUhST7 -oPޠ*)U T7 -oPڔU T7 -mJUxU T6*AUxU BRޠ*AUxUM -oPޠ*AUxЦT7 -oPޠ*AUXӪT7 -oPޠ*)U T7 -oPڔU T7 -mJUxU T6*AUxU BRޠ*AUxUM -oPޠ*AUxЦT7 -oPޠ*AUhST7 -oPޠ*)U T7 -oPִU T7 -mJUxU T6*AUxU BRޠ*AUxUM -oP| ܠ*~1UqH&UƛiU/T*>8Tū8~rQ?Ǝ,yNoˎ_mQW=|\Lr~RE?&7W)}}zrnk{xy:wI'7||ɻ?4C@bONjujÞl_&<>uJח; +xMdG=?E(u + -FhaZTfS-`F1OyN$b,zaLfvßn=x4sW25zi<5 1o-gyn+f2疮[̓o5>&d ۵s=E}^`jU-]k8nN|b$S_8^DԘ~]<8kKyn4kک5u8s^i<5>ױWZ<17i<5>k1ϵx;5kSSsi<5>Jz'2{̓^`rk|B3]r=^&t}7W=kvE!-Oʜi<_t}k|jS8yU+WO̓۹=~ʼgU[ǯEgM?Ԙ1xjo:xj|-_xj|W5Z<:ݚS`-os8jZ&5qT25WxVBq<2+]V浧=qnH<-1o۞Yc Vfo|5߽[O>5ֲL{6s|<;Ϛ~xjyf-]kg̓5yV{{5־,:.oL{zg̓&<yht}L +kՖ5L{ҽ=ԞgK{<=سګy=݃=k<#J`Ϛkݿ=~-?jMH`5o[?j)eOG̓DZ_i_W2[9|,S`kt}69=~-U1\05vi<5H{G̓5-_'{fx0:iyIt}~?jj`8ZզQyt}~xspˬX&Sc~!͒y|ԵϏ뺹=&^\tK?j|2^kW?j-^kkǑ>j/\\lgҧL`kkQo{_5#݃jϴǿj\fK`|=U`͂tyg[_5{L`L{G\|_5֯+ƥUU3J_5֪W̓ǯL\xj,J{ZvyO>5O.{k?'׷7Aǰc_>]R^ÿt;\5Y߿z/Z?˿;77ӿǟy }FFoSszy۷g\3֏|?w<8,; 㻸; %tJ,Y: %tJ,Y: %tJ,Y: 7tn,Y: 7tn,Y: 7tn,Y: 7tn,Y: 7tn,Y: 7tn,Y: 7tn,Y: 7tn,Y: 7tn,Y: 7tn,Y: 7tn,Y: 7tn,Y: 7tn,Y: 7tn,Y: 7tn,Y: 7tn,Y: 7tn,Y: 7tn,Y: 7tn,Y: 7tn,Y: 7tn,Y: 7tn,Y: 7tn,Y: 7tn,Y9,YpCgB,YpCgB,YpCgB,YpCgB,YpCgB,YpCgafvn,YpCgftn,YpCgftn,YpCgftn,YpCgftn,YpCgftn,YpCgftn,YpCgftn,YpCgftn,YpCgftn,YpCgafvn,YpCgftn,YpCgftn,YpCgftn,YpCgftn,YpCgftn,YpCgftn,YpCgftn,YpCgftn,YpCgftn,YpCgafvn,YpCgftn,YpCgftn,YpCgftn,YpCgftn,YpCgftn,YpCgftn,YpCgftn,YpCgftn,YpCgftn,YpCgafvn,YpCgftn,YpCgftn,YpCgftn,YpCgftn,t>C XiQZKw_6xFi)?-K\G}kOG2=~|wiB J #PBiB J #PBiB J #pCi J 5pCi J 5pCi J 5pCi J 5J 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZ9(-PZpCiB(-PZpCiB(-PZpCiB(-PZpCiB(-PZpCiB(-PZpCi,-PZpCiB(-PZpCiB(-PZpCiB(-PZpCiBAi J 7jFi٥J 7jFi· J 5pCi J 5pCi J 5pCi YZpCi QZpCi QZpCi QZpCi QZpCi QZpCi QZpCi QZpCi QZpCi QZpCi YZpCi QZpCi QZpCi QZpCi QZpCi QZpCi QZpCi QZpCi QZpCi QZpCi YZpCi QZpCi QZpCi QZpCi QZpCi QZpCi QZpCi QZpCi QZpCi QZpCi YZpCi QZpCi QZpCi QZpCi QZ0 /q_ZYZ|⯿QZ|zwi +gxoq}{l]u~x AOu3j:,g|~J3Pg(02 %J3Pg(02 %J3Pg(02 7n3PgP3 7n3PgP3 7n3PgP3 7n3PgP3 7n3PgP3 7n3PgP3 7n3PgP3 7n3PgP3 7n3PgP3 7n3Pg03 7n3PgP3 7n3PgP3 7n3PgP3 7n3PgP3 7n3PgP3 7n3PgP3 7n3PgP3 7n3PgP3 7n3PgP3 7n3Pg03 7n3PgP3 7n3PgP3 7n3PgP3 7n3PgP3 7n3PgP3 7nή3PgP3 7n-ou 7jF:u 7jF:u 7jF:u 3pC: u 5pC: u 5pC: u 5pC: u 5pC: u 5pC: u 5pC: u 5pC: u 5pC: u 5/ 7n3Pg03 7n3PgP3 7n3PgP3 7n3PgP3 7n3PgP3 7n3PgP3 7n3PgP3 7n3PgP3 7n3PgP3 7n3PgPpC: u 3pC: u 5pC: u 5pC: u 5pC: u 5pC: u 5pC: u 5pC: u 5pC: u 5pC: u 5/ 7n3Pg03 7n3PgP3 7n3PgP3 7n3PgP3 7n3PgP3 7f~qA:Vg<Ϗm_ /~Ygo]g +cq~/6ty>Wb CitizO*-֭~x>x҂>B J %FFiB J %FFiB J %Ffi J 7jFi J 7jFi J 7jFi J 7jFi J 7jFi J 7jFi J 7jFi J 7jFi J 7jFi J 7ffi J 7jFi J 7jFi J 7jFi J 7jFi J 7jFi J 7jFi J 7jFi J 7jFi J 7jFi J 7ffi J 7jFi J 7jFi J 7jFi J 7jFi J 7jFi J 7jFi J 7jFi J 7jFi J 7jFi J 3pCi J 5pCi J 5pCi J 5pCi J 5pCi J 5pCi J 5pCi J 5pCi J 5pCi J 5pCi J 3pCi J 5pCi J 5pCi J 5pCi gjFi J 7.-Ԍ J 7n]ZJ 7n(-ܬ/|+-Ԍ J 7n]ZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 7n(-PZJ 38:BVZU,-^Z@Sϸxk1Kӥc}[P?>=g3Ϭi5۾JE?]T&~)YsVmnN5=)Y=,%kn*Y=kd *jupך /{IPWFւ`Zxz{x|/=>3k1Pus^Z lwDfyC@4{z5u^Bb`nWx%k1oEkÛƼ05k1ט-{qYfZkCn8֘k^cq]8֘k|1_&YcZkmKŹ<8ט#_7k̯ǕƼ&ZY}az-ִwܮ3kޫhKŚJz-wsƼmZZ71ܬ1+k+˙^5+)KfxqXSҙ`xvYc>[z-,xD 5y17ŵ"ŶzPr|=Zlk<+ۚd֘4T͚ƼujiPHm̓HŚk֫ۚ9W5WZnk|ܶ^ݮ #y̓Ab̓;9W5X`zu[`Jzu[`m45[5W5֢6W5;X`-&zu[`Mriy.iy.@iy~c9n'xGgio?i@]^<=y̓֫Zk|<iy|kO}̓;vX-W5Xuzu_u~L}̓׶ik*^5^;(k=W5^#W5ִ֫㸧1y:^o-W~+)ﯮxL~+)ﯮx\c~+)ﯮxdіi2G+zu#AJWW}h6_]H1WWxڏoх.ttᤣ %DN:pх.ttIGN:pх.".". E0]E0]х`:`: 7DttLGttLGn.".".]E0]E0]!`:`:pCtLGttLGtt".". E0]E0]х]E0]E0]!`:`:pCtLGttLGtt".". E0]E0]х`:`: 7DttLGttLGn.".".]E0]E0]!`:`:P3`:`: 7DttLGttLGn.".".]E0]E0]!`:`:pCtLGttLGtt".". E0]E0]х`:`: 7DttLGttfDttLGttLGn]E0]E0]!`:`:pCtLGttLGtt".". E0]E0]х`:`: 7DttLGttLGn.".".]E0]E0]E0]E0]х{GttLGttLGn.".".]E0]E0]!`:`:pCtLGttLGtt".". E0]E0]х`:`: 7DttLGttLGjFtLGttLGttE0]E0]х`:`: 7DttLGttLGn.".".]E0]E0]!`:`:pCtLGttLGtt".". E0]E0]х]E0]E0]wtLGttLGtt".". E0]E0]х`:P +ԃ/DbZ)]||.]|~wtqXN_.ϡof}׿ܗy?aF=~e^<%ͧ2?, %J(3Pfe %J(3Pfe %J(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(3Pfe 7n(32 e 7n(3)3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2,3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2,3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2,3PfpC2C(3PfpC2C(3PfpC2C(3PfpC2C(3Pfy}4o2^}-\Vfw_6ˌG.3>=[*3/N5ƺ{{'6j Tcx+{k %J1Pc5j %J1Pc5j %J1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1Pc5j 7n1 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1Ԍ 5j 7n1Ԍ 5_a!|u5n-o;k/~5ƕkO1^XkMVv[gm,|Vze|ZO,xx>x _=ܕ* %TJ,B * %TJ,B * %TJ,Ge * 7TjFe * 7TjFe * 7TjFe * 7TjFe * 7TjFe * 7TjFe * 7TjFe * 7TjFe * 7TjFe * 7Tffe * 7TjFe * 7TjFe * 7TjFe * 7TjFe * 7TjFe * 7TjFe * 7TjFe * 7TjFe * 7TjFe * 7Tffe * 7TjFe * 7TjFe * 7TjFe * 7TjFe * 7TjFe * 7TjFe * 7TjFe * 7TjFe * 7TjFe * 3pCe * 5pCe * 5pCe * 5pCe * 5pCe * 5pCe * 5pCe * 5pCe * 5pCe * 5pCe * 3pCe * 5pCe * 5pCe * 5pCe * 5pCe * 5pCe * 5pCe * 5pCe * 5pCe * 5pCe * 3pCe * 5pCe * 5pCe * 5pCe * 5pCe * 5pCe * 5pCe * 5pCe * 5pCe * 5pCe * 3pCe * 5pCe * 5pCe * 5pCe * 5pCeaOs*_Xeq%uղK;+/~3Wm}q*?q|+-SԂz}nz|zg_=p1)tLIN:p1)S8IN:p1S1E0S1E0S!c`:c`:pCLLtLLtLᆘ")") 1E0S1E0S1b`:c`:c +7tLLtLLn)")")S1E0S1E0S!c`:c`:P3b`:c`:c +7tLLtLLn)")")S1E0S1E0S!c`:c`:pCLLtLLtLᆘ")") 1E0S1E0S1b`:c`:c +7tLLtLLjFLLtLLtLᆘ")") 1E0S1E0S1b`:c`:c +7tLLtLLn)")")S1E0S1E0S!c`:c`:pCLLtLLtLᆘ")")Ԍ")") 1E0S1E0S1b`:c`:c +7tLLtLLn)")")S1E0S1E0S!c`:c`:pCLLtLLtLᆘ")") 1E0S1E0S1S1E0S1E0S!c`:c`:pCLLtLLtLᆘ")") 1E0S1E0S1b`:c`:c +7tLLtLLn)")")S1E0S1E0S!c`:c`:P3b`:c`:c +7tLLtLLn)")")S1E0S1E0S!c`:c`:pCLLtLLtLᆘ")") 1E0S1E0S1b`:c`:c +7tLLtLLjFLLtLLtLᆘ")") 1E0S1E0S1b`:c`:c +7tLGi_)~Q11Fm/S||)^1l_og2S痏X7]|**xSQY?|kJ(*\]T(02 +%J(*\]T(02 +%J(*\]T(02 +7n(*\]TP3 +7n(*\]TP3 +7n(*PTP3 +7n(*PTP3 +7n(*PTP3 +7n(*PTP3 +7n(*PTP3 +7n(*PTP3 +7n(*PTP3 +7n(*PT03 +7n(*PTP3 +7n(*PTP3 +7n(*PTP3 +7n(*PTP3 +7n(*PTP3 +7n(*PTP3 +7n(*PTP3 +7n(*PTP3 +7n(*PTP3 +7n(*PT03 +7n(*PTP3 +7n(*PTP3 +7n(*PTPSTpCQᆢ EQTpCQᆢ EQTpCQᆢ EQTpCQᆢ EQTpCQᆢ EQTpCQᆢ EQTpCQᆢ,*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢ,*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢ,*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢ,*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PTpCQᆢB(*PT9}ooRQYT||/*O϶u}$**^緿__ +AVO}{x>xǷ_ma +%J+B a +%J+B a +%J+̰ a +7n+Ԍ a +7n+Ԍ a +7n+n+V!pCXfn+V!pCXfn+V!pCXfn+V!pCXfn+V!pCXfn+V!pCXfn+V!pCXafn+V!pCXfn+V!pCXfn+V!pCXf'pCXᆰ a +5#pCXᆰ a +5#pCXᆰ a +5#pCXᆰ a +5#pCXᆰ a +5#pCXᆰ a +5#pCXᆰ a +33pCXᆰ a +5#pCXᆰ a +5#pCXᆰ a +5;a +7n+Va +7n+Va +7n+Va +7n+Va +7n+Va +7n+Va +7n+̰ a +7n+Ԍ a +7n+Ԍ a +7n+Ԍ a +7n+Ԍ a +7n+Ԍ a +7n+Ԍ a +7n+Ԍ a +7n+Ԍ a +7n+Ԍ a +7n+̰ a +7n+Ԍ a +7n+Ԍ a +7n+Ԍ a +7n+Ԍ a +7n+Ԍ a +7n+Ԍ a +7n+Ԍ a +7n+Ԍ a +7n+Ԍ a +7n+̰ a +7n+Ԍ a +7n+Ԍ a +7n+Ԍ a +7n+Ԍ a +7n+Ԍ a +7n+Ԍ a +7n+Ԍ a +7n+Ԍ a +7n+Ԍ a +7n+̰ a +7n+Ԍ a +7n+Ԍ a +7n+Ԍ a +7n+Ԍ a_G'|!u5֚eoV||+5-9~-jy2Sß*52~|w +C * #PB +C * #PB +C * #pC + * 5pC + * 5pC + * 5pC + * 5pC + * 5pC + * 5pC + * 5pC + * 5pC + * 5pC + * 3pC + * 5pC + * 5pC + * 5pC + * 5pC + * 5pCv3* 7TjF + * 7TjF + * 7TjF + * 7TjF + * 7Tff + * 7TjF + * 70Ԍ + * 7Tn^]a* 7Tn0ܼP3* 7Tn0PayufTn0PapCfTn0PapCfTn0PapCfTn0PapCfTn0Pa03+ 7Tn0PaP3* 7Tn0PaP3* 7Tn0PaP3* 7Tn0PaP3* 7Tn0PaP3* 7Tn0PaP3* 7Tn0PaP3* 7Tn0PaP3* 7Tn0PaP3* 7Tn0Pa03+ 7Tn0PaP3* 7Tn0PaP3* 7Tn0PaP3* 7Tn0PaP3* 7Tn0PaP3* 7Tn0PaP3* 7Tn0PaP3* 7Tn0PaP3* 7Tn0PaP3* 7Tn0Pa03+ 7Tn0PaP3* 7Tn0PaP3* 7Tn0PaP3* 7Tn0PaP3* 7Tn0PaP3* 7Tn0PaP3* 7Tn0PaP3* 7Tn0PaP3* 7Tn0PaP3* 7Tn0Pa03+ 7Tn0PaP3* 7Tn0PaP3* 7Tn0PaP3* 7Tn0PaP3* 7Tf~qvA +VaeZ ׯU0mWƧgKP{eqNhf1ǧ| ']`8IJ(0t ']`8C .0t ']`(F0]`F0]` +` ` 7tLtLn(0#.0#.0P`F0]`F0]` ` `pCLtLt#.0#.0 F0]`F0]` +` ` 5 ` `pCLtLt#.0#.0 F0]`F0]` +` ` 7tLtLn(0#.0#.0P`F0]`F0]` ` `pCLtLtftLt{#.0#.0y}/0P`F0]`F0 +` `pCLtLn(0ٿtLt#{LtLn(0ٿtLt#{LtLn(0Y_xtLjFLtLt#.0#.0 F0]`F0]` +` ` 7tLtLn(0#.0#.0P`F0]`F0]` ` `pCLtLt#.0#.0C(0#.0#.0P`F0]`F0]` ` `pCLtLt#.0#.0 F0]`F0]` +` ` 7tLtLn(0#.0#.0P`F0]`F0]`F0]`F0]` +` ` 7tLtLn(0#.0#.0P`F0]`F0]` ` `pCLtLt#.0#.0 F0]`F0]` +` ` 5 ` `pCLtLt#.0#.0 F0]`F0]` +`Pc_(0~aU~-֗ +,0mRaO_뇾ar&X^k5VϷSO5)xx>xǚ^B 5j +%FFMB 5j +%FFMB 5j +%FfMᆚ 5j +7jFMᆚ 5j +7jFMᆚ 5j +7jFMᆚ 5j +7jFMᆚ 5j +7jFMᆚ 5j +7jFMᆚ 5j +7jFMᆚ 5j +7jFMᆚ 5j +7jFMᆚ 5j +7ffMᆚ 5j +7jFMᆚ 5j +7jFMᆚ 5j +7jFMᆚ 5j +7jFMᆚ 5j +7jFMᆚ 5j +7jFMᆚ 5j +7jFMᆚ 5j +7jFMᆚ 5j +7jFMᆚ 5j +7ffMᆚ 5j +7jFMᆚ 5j +7jFMᆚ 5j +7jFMᆚ 5j +7jFMᆚ 5j +7jFMf 5j +7jFMf 5j +7jFMf 5j +7jFMf 5j +7jFMf([Mᆚ 5YSpCMᆚ 5QSpCMᆚ 5QSpCMᆚ 5QSpCMᆚ 5QSpCMᆚͳk +5pCMᆚ 5o5QSpCMᆚͳk +5pCMᆚ 5gjFMᆚ 5j +7Ϯ)Ԍ 5j +7n)̚ 5j +7n)Ԍ 5j +7n)Ԍ 5j +7n)Ԍ 5j +7n)Ԍ 5j +7n)Ԍ 5j +7n)Ԍ 5j +7n)Ԍ 5j +7n)Ԍ 5j +7n)Ԍ 5j +7n)̚ 5j +7n)Ԍ 5j +7n)Ԍ 5j +7n)Ԍ 5j +7n)Ԍ 5j +7n)Ԍ 5j +7n)Ԍ 5j +7n)Ԍ 5j +7n)Ԍ 5j +7n)Ԍ 5j +7n)̚ 5j +7n)Ԍ 5j +7n)Ԍ 5j +7n)Ԍ 5j +7n)Ԍ 5_AvM!)jW-TS\귙5őkOaǷ*j*}YT𧤢<~LJ=??]yo?yu/yWO`8}b ߋ7kxee 'C/ p2I 'C/ p2I 'C/ p2ɢG3h^Ћz&zq4C/f 8nG3h^Ћz&zq4C/f 8nG3h^Ћz&zq4C/f 8nG3h^Ћz&zq4C/f 8nG3h^Ћz&zq4C/f 8l8G3h^ 7ыzq4C/f p8G3h^ 7ыzq4C/f p8G3h^ 7ыzq4C/f p8G3h^ 7ыzq4C/f p8G3h^ 7ыzq4C/f ` 8G3h^^Ћzq4C/f腛 8G3h^^Ћzq4C/f腛 8G3h^^Ћzq4C/f腛 8G3h^^Ћzq4C/f腛 8G3h^^Ћzq4C/,zq4C/f 8nG3h^Ћz&zq4C/f 8nG3h^Ћz&zq4C/f 8nG3h^Ћz&zq4C/f 8nG3h^Ћz&zq4C/f 8nG3h^Ћzfыzq4C/f p8G3h^ 7ыzq4C/f p8G3h^ 7ыzq4C/f p8G3h^ 7ыzq4C/f p8G3h^ 7ыzq4C/f p8G3h^ 6 8G3h^^Ћzq4C/f腛 8G3h^^Ћzq4C/f腛 8G3h^^Ћzq4C/f腛 8G3h^^Ћzq4C/f腛 8G3h^^Ћzq4C/fE/f 8G3Mh^Ћzq4C/D/f 8G3Mh^Ћzq4C/D/f&,^A/9ԁw^|EzʋOŋU^CS<1#c?/}xm8hx_ὖ5=h; & & & %_0_0_0_0_(YIIIIBnnnnlpppp`/////,~&~&~&~&~f 7 7 7 7 6_____YMMMMnnnnlppppP/////,~&~&~&~&~f 7 7 7 7 6_____YMMMMnnnnlpppp`/////l~&~&~&~&~f 7 7 7 7 6_____YMMMMnnnnlpppp`/////,~&~&~&~&~f 7 7 7 5_____YMMMMnnnnlpppp`/////,~&~&~&~&~f 7 7 7 7 6_____YMMMMBnnnnlpppp`/////,~&~&~&~&~f 7 7 7 7 6_____YMMMMnnnnlppppP/////,~&~&~&~&~f 7 7 7 7 6_____YMMMMnnnnlpppp`/////l~&~&~&~&~f 7 7 7 7 6__m//6MMnj~`?9@oGO/^{1_|U1׷e3`<>}g^_㜆Z;S˾`0`0`0`(YIIIIC"L"L"L"L"J6pppp`E0D0D0D0D0,&&&&f 7 7 7 7 6`````YMMMM"n"n"n"n"lpppp`M0D0D0D0D0,&&&&f 7 7 7 7 6`````YMMMM"n"n"n"n"lpppp`E0D0D0D0D0,&&&&f 7 7 7 7 6`````YMMMM"n"n"n"n"lpppp`E0D0D0D0D0,&&&&f 7 7 7 7 6````MMMM"n"n"n"n"lpppp`E0D0D0D0D0,&&&&f 7 7 7 7 6`````YMMMM"n"n"n"n"j6pppp`E0D0D0D0D0,&&&&f 7 7 7 7 6`````YMMMM"n"n"n"n"lpppp`M0D0D0D0D0,&&&&f 7 7 7 7 6`````YMMMM"n"n"n"n"lpppp`E0D0D0D0D0,&&&&f 7 7 7 7 6`````YMMMM"n"n"n"n"lpP3+?xX"~`b6b~x>{׷/۟_+ULJ6_L U UkY-~U$U$U$Ud +& +& +& +& +%KU0IU0IU0IU0IU(٪MMMMRnRnRnRnRlpppp`TTTTT*ܤ*ܤ*ܤ*ܤ*,U&U&U&U&Uf +7 +7 +7 +7 +6KUIUIUIUIUYMMMMRnRnRnRnRjpppp`TTTTT*ܤ*ܤ*ܤ*ܤ*,U&U&U&U&Uf +7 +7 +7 +7 +6KUIUIUIUIUYMMMMRnRnRnRnRlpppp`TTTTT*ܤ*ܤ*ܤ*ܤ*,U&U&U&U&Uf +7 +7 +7 +7 +6KUIUIUIUIUYMMMMRnRnRnRnRlpppp`TTTTT*ܤ*ܤ*ܤ*ܤ*,U&U&U&Uf +7 +7 +7 +7 +6KUIUIUIUIUYMMMMRnRnRnRnRlpppp`TTTTT*ܤ*ܤ*ܤ*ܤ*,U&U&U&U&Uf +7 +7 +7 +7 +6KUIUIUIUIU٪MMMMRnRnRnRnRlpppp`TTTTT*ܤ*ܤ*ܤ*ܤ*,U&U&U&U&Uf +7 +7 +7 +7 +6KUIUIUIUIUYMMMMRnRnRnRnRjpppp`TTTTT*ܤ*ܤ*ܤ*ܤ*,U&U&U&U&Uf +7 +7 +7 +7 +6KUIUIUIUIUYMMMMRnRnRnRnRlpppp`TTTTT*ܤ*ܤ*ܤ*ܤ*,U&U&U&U&Uf +7 +7 +7 +7 +6KUIUIUIUIUYMBs̴PkǏχ?U>xlUOTŋU~QO&k&dTo?|a./TE/Tkxe}vTTTT*****,U$U$U$U$Ud +7 +7 +7 +7 +6KUIUIUIUIUYMMMMRnRnRnRnRlpppp`TTTTT*ܤ*ܤ*ܤ*ܤ*,U&U&U&U&Uf +7 +7 +7 +7 +6KUIUIUIUIU٪MMMMRnRnRnRnRlpppp`TTTTT*ܤ*ܤ*ܤ*ܤ*,U&U&U&U&Uf +7 +7 +7 +7 +6KUIUIUIUIUYMMMMRnRnRnRnRjpppp`TTTTT*ܤ*ܤ*ܤ*ܤ*,U&U&U&U&Uf +7 +7 +7 +7 +6KUIUIUIUIUYMMMMRnRnRnRnRlpppp`TTTT*ܤ*ܤ*ܤ*ܤ*,U&U&U&U&Uf +7 +7 +7 +7 +6KUIUIUIUIUYMMMMRnRnRnRnRlpppp`TTTTT*ܤ*ܤ*ܤ*ܤ*,U&U&U&U&Uf +7 +7 +7 +7 +6KUIUIUIUIUYMMMMRnRnRnRnRlpppp`TTTTT*ܤ*ܤ*ܤ*ܤ*,U&U&U&U&Uf +7 +7 +7 +7 +6KUIUIUIUIU٪MMMMRnRnRnRnRlpppp`TTTTT*ܤ*ܤ*ܤ*ܤ*,U&U&U&U&Uf +7 +7 +7 +7 +6KUIUIUIUIUYMMMMRnRnRnRnRjpppp`TTTTT*ܤ*ܤ*ܤ*ܤ*,U&U&U&U&Uf +7 +5?E0BUs燘oSQUb|c"'},sW=/˞.{݄Gqzqm~LA}o=Scuk|~~?mU߯su~$/lk/ϯٞ?'ʧw=ƽ}hw{6?a'Nj/|;}b>AO_wϛ}eJ7mmy{{| endstream endobj 115 0 obj @@ -2108,7 +2083,7 @@ endobj /MediaBox [ 0 0 595.275591 841.889764 ] /Contents 114 0 R /Resources 4 0 R -/Annots [ 116 0 R 117 0 R 118 0 R 119 0 R 120 0 R 121 0 R 122 0 R 123 0 R 124 0 R 125 0 R 126 0 R 127 0 R 128 0 R 129 0 R 130 0 R 131 0 R 132 0 R 133 0 R 134 0 R 135 0 R 136 0 R 137 0 R 138 0 R 139 0 R 140 0 R 141 0 R 142 0 R 143 0 R 144 0 R 145 0 R 146 0 R 147 0 R 148 0 R 149 0 R 150 0 R 151 0 R 152 0 R 153 0 R 154 0 R 155 0 R 156 0 R 157 0 R 158 0 R 159 0 R 160 0 R 161 0 R 162 0 R 163 0 R 164 0 R 165 0 R 166 0 R 167 0 R 168 0 R 169 0 R 170 0 R 171 0 R 172 0 R 173 0 R 174 0 R 175 0 R 176 0 R 177 0 R 178 0 R 179 0 R 180 0 R 181 0 R 182 0 R 183 0 R 184 0 R ] +/Annots [ 116 0 R 117 0 R 118 0 R 119 0 R 120 0 R 121 0 R 122 0 R 123 0 R 124 0 R 125 0 R 126 0 R 127 0 R 128 0 R 129 0 R 130 0 R 131 0 R 132 0 R 133 0 R 134 0 R 135 0 R 136 0 R 137 0 R 138 0 R 139 0 R 140 0 R 141 0 R 142 0 R 143 0 R 144 0 R 145 0 R 146 0 R 147 0 R 148 0 R 149 0 R 150 0 R 151 0 R 152 0 R 153 0 R 154 0 R 155 0 R 156 0 R 157 0 R 158 0 R 159 0 R 160 0 R 161 0 R 162 0 R 163 0 R 164 0 R 165 0 R 166 0 R 167 0 R 168 0 R 169 0 R 170 0 R 171 0 R 172 0 R 173 0 R 174 0 R 175 0 R 176 0 R 177 0 R 178 0 R 179 0 R 180 0 R 181 0 R 182 0 R 183 0 R 184 0 R 185 0 R 186 0 R 187 0 R 188 0 R 189 0 R 190 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> @@ -2121,7 +2096,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:recrutement-payerne-mai-2023) +/Dest (jdb/:lundi-1-mai-2023) >> endobj 117 0 obj @@ -2132,7 +2107,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:recrutement-payerne-mai-2023) +/Dest (jdb/:lundi-1-mai-2023) >> endobj 118 0 obj @@ -2143,7 +2118,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:recrutement-payerne-mai-2023) +/Dest (jdb/:lundi-1-mai-2023) >> endobj 119 0 obj @@ -2154,7 +2129,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:vendredi-5-mai-2023) +/Dest (jdb/:mardi-2-mai-2023) >> endobj 120 0 obj @@ -2165,7 +2140,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:vendredi-5-mai-2023) +/Dest (jdb/:mardi-2-mai-2023) >> endobj 121 0 obj @@ -2176,7 +2151,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:vendredi-5-mai-2023) +/Dest (jdb/:mardi-2-mai-2023) >> endobj 122 0 obj @@ -2187,7 +2162,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:lundi-8-mai-2023) +/Dest (jdb/:recrutement-payerne-mai-2023) >> endobj 123 0 obj @@ -2198,7 +2173,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:lundi-8-mai-2023) +/Dest (jdb/:recrutement-payerne-mai-2023) >> endobj 124 0 obj @@ -2209,7 +2184,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:lundi-8-mai-2023) +/Dest (jdb/:recrutement-payerne-mai-2023) >> endobj 125 0 obj @@ -2220,7 +2195,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:mardi-9-mai-2023) +/Dest (jdb/:vendredi-5-mai-2023) >> endobj 126 0 obj @@ -2231,7 +2206,7 @@ endobj /BS << /W 0 >> -/Dest (jdb/:mardi-9-mai-2023) +/Dest (jdb/:vendredi-5-mai-2023) >> endobj 127 0 obj @@ -2242,29 +2217,29 @@ endobj /BS << /W 0 >> -/Dest (jdb/:mardi-9-mai-2023) +/Dest (jdb/:vendredi-5-mai-2023) >> endobj 128 0 obj << /Type /Annot /Subtype /Link -/Rect [ 45.466457 698.583622 557.809134 680.473622 ] +/Rect [ 53.466457 698.583622 557.809134 680.473622 ] /BS << /W 0 >> -/Dest (INXWIZI/) +/Dest (jdb/:lundi-8-mai-2023) >> endobj 129 0 obj << /Type /Annot /Subtype /Link -/Rect [ 45.466457 694.023622 59.641994 681.223622 ] +/Rect [ 53.466457 694.023622 77.821193 681.223622 ] /BS << /W 0 >> -/Dest (INXWIZI/) +/Dest (jdb/:lundi-8-mai-2023) >> endobj 130 0 obj @@ -2275,7 +2250,7 @@ endobj /BS << /W 0 >> -/Dest (INXWIZI/) +/Dest (jdb/:lundi-8-mai-2023) >> endobj 131 0 obj @@ -2286,18 +2261,18 @@ endobj /BS << /W 0 >> -/Dest (Code/ConfigurationTool/:configurationtoolcs) +/Dest (jdb/:mardi-9-mai-2023) >> endobj 132 0 obj << /Type /Annot /Subtype /Link -/Rect [ 53.466457 675.913622 72.731593 663.113622 ] +/Rect [ 53.466457 675.913622 77.821193 663.113622 ] /BS << /W 0 >> -/Dest (Code/ConfigurationTool/:configurationtoolcs) +/Dest (jdb/:mardi-9-mai-2023) >> endobj 133 0 obj @@ -2308,29 +2283,29 @@ endobj /BS << /W 0 >> -/Dest (Code/ConfigurationTool/:configurationtoolcs) +/Dest (jdb/:mardi-9-mai-2023) >> endobj 134 0 obj << /Type /Annot /Subtype /Link -/Rect [ 53.466457 662.363622 557.809134 644.253622 ] +/Rect [ 45.466457 662.363622 557.809134 644.253622 ] /BS << /W 0 >> -/Dest (Code/DriverGapToLeaderWindow/:drivergaptoleaderwindowcs) +/Dest (INXWIZI/) >> endobj 135 0 obj << /Type /Annot /Subtype /Link -/Rect [ 53.466457 657.803622 72.731593 645.003622 ] +/Rect [ 45.466457 657.803622 59.641994 645.003622 ] /BS << /W 0 >> -/Dest (Code/DriverGapToLeaderWindow/:drivergaptoleaderwindowcs) +/Dest (INXWIZI/) >> endobj 136 0 obj @@ -2341,7 +2316,7 @@ endobj /BS << /W 0 >> -/Dest (Code/DriverGapToLeaderWindow/:drivergaptoleaderwindowcs) +/Dest (INXWIZI/) >> endobj 137 0 obj @@ -2352,7 +2327,7 @@ endobj /BS << /W 0 >> -/Dest (Code/DriverPositionWindow/:driverpositionwindowcs) +/Dest (Code/ConfigurationTool/:configurationtoolcs) >> endobj 138 0 obj @@ -2363,7 +2338,7 @@ endobj /BS << /W 0 >> -/Dest (Code/DriverPositionWindow/:driverpositionwindowcs) +/Dest (Code/ConfigurationTool/:configurationtoolcs) >> endobj 139 0 obj @@ -2374,7 +2349,7 @@ endobj /BS << /W 0 >> -/Dest (Code/DriverPositionWindow/:driverpositionwindowcs) +/Dest (Code/ConfigurationTool/:configurationtoolcs) >> endobj 140 0 obj @@ -2385,7 +2360,7 @@ endobj /BS << /W 0 >> -/Dest (Code/F1TVEmulator/:f1tvemulatorcs) +/Dest (Code/DriverGapToLeaderWindow/:drivergaptoleaderwindowcs) >> endobj 141 0 obj @@ -2396,7 +2371,7 @@ endobj /BS << /W 0 >> -/Dest (Code/F1TVEmulator/:f1tvemulatorcs) +/Dest (Code/DriverGapToLeaderWindow/:drivergaptoleaderwindowcs) >> endobj 142 0 obj @@ -2407,7 +2382,7 @@ endobj /BS << /W 0 >> -/Dest (Code/F1TVEmulator/:f1tvemulatorcs) +/Dest (Code/DriverGapToLeaderWindow/:drivergaptoleaderwindowcs) >> endobj 143 0 obj @@ -2418,7 +2393,7 @@ endobj /BS << /W 0 >> -/Dest (Code/Program/:programcs) +/Dest (Code/DriverPositionWindow/:driverpositionwindowcs) >> endobj 144 0 obj @@ -2429,7 +2404,7 @@ endobj /BS << /W 0 >> -/Dest (Code/Program/:programcs) +/Dest (Code/DriverPositionWindow/:driverpositionwindowcs) >> endobj 145 0 obj @@ -2440,7 +2415,7 @@ endobj /BS << /W 0 >> -/Dest (Code/Program/:programcs) +/Dest (Code/DriverPositionWindow/:driverpositionwindowcs) >> endobj 146 0 obj @@ -2451,7 +2426,7 @@ endobj /BS << /W 0 >> -/Dest (Code/Window/:windowcs) +/Dest (Code/F1TVEmulator/:f1tvemulatorcs) >> endobj 147 0 obj @@ -2462,7 +2437,7 @@ endobj /BS << /W 0 >> -/Dest (Code/Window/:windowcs) +/Dest (Code/F1TVEmulator/:f1tvemulatorcs) >> endobj 148 0 obj @@ -2473,7 +2448,7 @@ endobj /BS << /W 0 >> -/Dest (Code/Window/:windowcs) +/Dest (Code/F1TVEmulator/:f1tvemulatorcs) >> endobj 149 0 obj @@ -2484,7 +2459,7 @@ endobj /BS << /W 0 >> -/Dest (Code/DriverData/:driverdatacs) +/Dest (Code/Program/:programcs) >> endobj 150 0 obj @@ -2495,7 +2470,7 @@ endobj /BS << /W 0 >> -/Dest (Code/DriverData/:driverdatacs) +/Dest (Code/Program/:programcs) >> endobj 151 0 obj @@ -2506,7 +2481,7 @@ endobj /BS << /W 0 >> -/Dest (Code/DriverData/:driverdatacs) +/Dest (Code/Program/:programcs) >> endobj 152 0 obj @@ -2517,7 +2492,7 @@ endobj /BS << /W 0 >> -/Dest (Code/DriverLapTimeWindow/:driverlaptimewindowcs) +/Dest (Code/Window/:windowcs) >> endobj 153 0 obj @@ -2528,7 +2503,7 @@ endobj /BS << /W 0 >> -/Dest (Code/DriverLapTimeWindow/:driverlaptimewindowcs) +/Dest (Code/Window/:windowcs) >> endobj 154 0 obj @@ -2539,7 +2514,7 @@ endobj /BS << /W 0 >> -/Dest (Code/DriverLapTimeWindow/:driverlaptimewindowcs) +/Dest (Code/Window/:windowcs) >> endobj 155 0 obj @@ -2550,7 +2525,7 @@ endobj /BS << /W 0 >> -/Dest (Code/DriverSectorWindow/:driversectorwindowcs) +/Dest (Code/DriverData/:driverdatacs) >> endobj 156 0 obj @@ -2561,7 +2536,7 @@ endobj /BS << /W 0 >> -/Dest (Code/DriverSectorWindow/:driversectorwindowcs) +/Dest (Code/DriverData/:driverdatacs) >> endobj 157 0 obj @@ -2572,7 +2547,7 @@ endobj /BS << /W 0 >> -/Dest (Code/DriverSectorWindow/:driversectorwindowcs) +/Dest (Code/DriverData/:driverdatacs) >> endobj 158 0 obj @@ -2583,18 +2558,18 @@ endobj /BS << /W 0 >> -/Dest (Code/Form1/:form1cs) +/Dest (Code/DriverLapTimeWindow/:driverlaptimewindowcs) >> endobj 159 0 obj << /Type /Annot /Subtype /Link -/Rect [ 53.466457 512.923622 77.821193 500.123622 ] +/Rect [ 53.466457 512.923622 72.731593 500.123622 ] /BS << /W 0 >> -/Dest (Code/Form1/:form1cs) +/Dest (Code/DriverLapTimeWindow/:driverlaptimewindowcs) >> endobj 160 0 obj @@ -2605,7 +2580,7 @@ endobj /BS << /W 0 >> -/Dest (Code/Form1/:form1cs) +/Dest (Code/DriverLapTimeWindow/:driverlaptimewindowcs) >> endobj 161 0 obj @@ -2616,18 +2591,18 @@ endobj /BS << /W 0 >> -/Dest (Code/Reader/:readercs) +/Dest (Code/DriverSectorWindow/:driversectorwindowcs) >> endobj 162 0 obj << /Type /Annot /Subtype /Link -/Rect [ 53.466457 494.813622 77.821193 482.013622 ] +/Rect [ 53.466457 494.813622 72.731593 482.013622 ] /BS << /W 0 >> -/Dest (Code/Reader/:readercs) +/Dest (Code/DriverSectorWindow/:driversectorwindowcs) >> endobj 163 0 obj @@ -2638,7 +2613,7 @@ endobj /BS << /W 0 >> -/Dest (Code/Reader/:readercs) +/Dest (Code/DriverSectorWindow/:driversectorwindowcs) >> endobj 164 0 obj @@ -2649,7 +2624,7 @@ endobj /BS << /W 0 >> -/Dest (Code/Zone/:zonecs) +/Dest (Code/Form1/:form1cs) >> endobj 165 0 obj @@ -2660,7 +2635,7 @@ endobj /BS << /W 0 >> -/Dest (Code/Zone/:zonecs) +/Dest (Code/Form1/:form1cs) >> endobj 166 0 obj @@ -2671,7 +2646,7 @@ endobj /BS << /W 0 >> -/Dest (Code/Zone/:zonecs) +/Dest (Code/Form1/:form1cs) >> endobj 167 0 obj @@ -2682,7 +2657,7 @@ endobj /BS << /W 0 >> -/Dest (Code/DriverDrsWindow/:driverdrswindowcs) +/Dest (Code/Reader/:readercs) >> endobj 168 0 obj @@ -2693,7 +2668,7 @@ endobj /BS << /W 0 >> -/Dest (Code/DriverDrsWindow/:driverdrswindowcs) +/Dest (Code/Reader/:readercs) >> endobj 169 0 obj @@ -2704,7 +2679,7 @@ endobj /BS << /W 0 >> -/Dest (Code/DriverDrsWindow/:driverdrswindowcs) +/Dest (Code/Reader/:readercs) >> endobj 170 0 obj @@ -2715,7 +2690,7 @@ endobj /BS << /W 0 >> -/Dest (Code/DriverNameWindow/:drivernamewindowcs) +/Dest (Code/Zone/:zonecs) >> endobj 171 0 obj @@ -2726,7 +2701,7 @@ endobj /BS << /W 0 >> -/Dest (Code/DriverNameWindow/:drivernamewindowcs) +/Dest (Code/Zone/:zonecs) >> endobj 172 0 obj @@ -2737,7 +2712,7 @@ endobj /BS << /W 0 >> -/Dest (Code/DriverNameWindow/:drivernamewindowcs) +/Dest (Code/Zone/:zonecs) >> endobj 173 0 obj @@ -2748,7 +2723,7 @@ endobj /BS << /W 0 >> -/Dest (Code/DriverTyresWindow/:drivertyreswindowcs) +/Dest (Code/DriverDrsWindow/:driverdrswindowcs) >> endobj 174 0 obj @@ -2759,7 +2734,7 @@ endobj /BS << /W 0 >> -/Dest (Code/DriverTyresWindow/:drivertyreswindowcs) +/Dest (Code/DriverDrsWindow/:driverdrswindowcs) >> endobj 175 0 obj @@ -2770,7 +2745,7 @@ endobj /BS << /W 0 >> -/Dest (Code/DriverTyresWindow/:drivertyreswindowcs) +/Dest (Code/DriverDrsWindow/:driverdrswindowcs) >> endobj 176 0 obj @@ -2781,7 +2756,7 @@ endobj /BS << /W 0 >> -/Dest (Code/OcrImage/:ocrimagecs) +/Dest (Code/DriverNameWindow/:drivernamewindowcs) >> endobj 177 0 obj @@ -2792,7 +2767,7 @@ endobj /BS << /W 0 >> -/Dest (Code/OcrImage/:ocrimagecs) +/Dest (Code/DriverNameWindow/:drivernamewindowcs) >> endobj 178 0 obj @@ -2803,7 +2778,7 @@ endobj /BS << /W 0 >> -/Dest (Code/OcrImage/:ocrimagecs) +/Dest (Code/DriverNameWindow/:drivernamewindowcs) >> endobj 179 0 obj @@ -2814,7 +2789,7 @@ endobj /BS << /W 0 >> -/Dest (Code/Settings/:settingscs) +/Dest (Code/DriverTyresWindow/:drivertyreswindowcs) >> endobj 180 0 obj @@ -2825,7 +2800,7 @@ endobj /BS << /W 0 >> -/Dest (Code/Settings/:settingscs) +/Dest (Code/DriverTyresWindow/:drivertyreswindowcs) >> endobj 181 0 obj @@ -2836,7 +2811,7 @@ endobj /BS << /W 0 >> -/Dest (Code/Settings/:settingscs) +/Dest (Code/DriverTyresWindow/:drivertyreswindowcs) >> endobj 182 0 obj @@ -2847,7 +2822,7 @@ endobj /BS << /W 0 >> -/Dest (Code/recoverCookiesCSV/:recovercookiescsvpy) +/Dest (Code/OcrImage/:ocrimagecs) >> endobj 183 0 obj @@ -2858,7 +2833,7 @@ endobj /BS << /W 0 >> -/Dest (Code/recoverCookiesCSV/:recovercookiescsvpy) +/Dest (Code/OcrImage/:ocrimagecs) >> endobj 184 0 obj @@ -2869,11 +2844,77 @@ endobj /BS << /W 0 >> -/Dest (Code/recoverCookiesCSV/:recovercookiescsvpy) +/Dest (Code/OcrImage/:ocrimagecs) >> endobj 185 0 obj << +/Type /Annot +/Subtype /Link +/Rect [ 53.466457 354.493622 557.809134 336.383622 ] +/BS << +/W 0 +>> +/Dest (Code/Settings/:settingscs) +>> +endobj +186 0 obj +<< +/Type /Annot +/Subtype /Link +/Rect [ 53.466457 349.933622 77.821193 337.133622 ] +/BS << +/W 0 +>> +/Dest (Code/Settings/:settingscs) +>> +endobj +187 0 obj +<< +/Type /Annot +/Subtype /Link +/Rect [ 542.540335 349.933622 557.809134 337.133622 ] +/BS << +/W 0 +>> +/Dest (Code/Settings/:settingscs) +>> +endobj +188 0 obj +<< +/Type /Annot +/Subtype /Link +/Rect [ 53.466457 336.383622 557.809134 318.273622 ] +/BS << +/W 0 +>> +/Dest (Code/recoverCookiesCSV/:recovercookiescsvpy) +>> +endobj +189 0 obj +<< +/Type /Annot +/Subtype /Link +/Rect [ 53.466457 331.823622 77.821193 319.023622 ] +/BS << +/W 0 +>> +/Dest (Code/recoverCookiesCSV/:recovercookiescsvpy) +>> +endobj +190 0 obj +<< +/Type /Annot +/Subtype /Link +/Rect [ 542.540335 331.823622 557.809134 319.023622 ] +/BS << +/W 0 +>> +/Dest (Code/recoverCookiesCSV/:recovercookiescsvpy) +>> +endobj +191 0 obj +<< /Filter /FlateDecode /Length 3021 >> @@ -2892,19 +2933,19 @@ r0x! CQ(maQnG MyK#JrS̰.LWBTIo!3R=$4T(A8rNS̤¹6$*U(,j rG G W[ĝUCSI\)d%9ku-$t#*r*#*rpS– 5L0B?`-C[L+#?F{~}4Nj~ٟx!}ʾOݷ.]y?_2ĸz`Y57:>34s/ge<΄gL޿<{'':{pH;q n'oN87Os:vzW#"7urg:2x/~mZRs~]1z]ϯ{=bϷ䗯u_K^~χc]a{;?ΫuԉM#I?˲?9z:k_5_ ǷF/xyzC|pmߨ۳a|Vߛdjqw>潶No~gݎ\wMswW71|g'B=h@ns8ޜKL^|b endstream endobj -186 0 obj +192 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 185 0 R +/Contents 191 0 R /Resources 4 0 R -/Annots [ 187 0 R ] +/Annots [ 193 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -187 0 obj +193 0 obj << /Type /Annot /Subtype /Link @@ -2919,7 +2960,7 @@ endobj >> >> endobj -188 0 obj +194 0 obj << /Filter /FlateDecode /Length 392 @@ -2928,19 +2969,19 @@ stream xRn0 +UIIȡvv)v؆tiwkEӶ(||$Fp@)cR5w1@٦w=$VhXբ&DFT$>M.%s*hUK1\n1A|hN"lʫŴ9:ZV;l؋Y|B:;_Tt2UdNF?Ord*țuÂ;Rl@b88ۯ߾8+zȟٍBfWbI}:= }]%!72IZ+!DQzV(Bms0AyuZIL۔6`Ňu LOwvCü;<A)T endstream endobj -189 0 obj +195 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 188 0 R +/Contents 194 0 R /Resources 4 0 R -/Annots [ 190 0 R ] +/Annots [ 196 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -190 0 obj +196 0 obj << /Type /Annot /Subtype /Link @@ -2955,38 +2996,37 @@ endobj >> >> endobj -191 0 obj +197 0 obj << /Filter /FlateDecode -/Length 4316 +/Length 4335 >> stream -x͜K+rm&^7^hcEU1+#3Kґt#97nJ/{OT5#n6_S,SiJ4Z}m 1Nbt7O~S?8J-C̷\#^.;7Znu._T/Ԧfs0oݹ]wWg9|,0~,8`zn헛ln]+v6r}sacDO7O3@ZE@lj9пRتnιsH&\ G@7O![/ [:!R] BŬ [=# [S I@pkKLSH%KdےK -B- jvV،LmIlZJ -,% -4;!BɊwDK!d|񎘃O^0 |5!`!P9:C1T%#>s4$cEpӲR9%S -AGbNٔH̩*1lF$f1I 1#A'aoUb9Z0ĜSQ11JVsV%KYLjdp0+)dbImČj!+ndb{+bnF!fH 1[SHG_-Ձ%&5a$&8O)tR[@b.Gj 6-1Lb@ެ 1R ݬJ[> B}!H AKQA6b -|M,1ĜrM?ԩ%ؔbƕOnj0 -w%]CiS}&P) > 9>/1ʻ)hYQA_J >kQղҖr6!FS)+ >DFF 1*(nhYar | *Nb^AHȼ%!f>(苗ڂ/J 13 EqD_I 1Gr|0hR[$#9Oю,%Y*m`Hsb D|/J[b04Lbr?6jJ[.TU =蕶!b?d;-,_Vs!PC!bl*nƬJC SҖ2MU">|N (ҖV*E|UV$# @d&-dU#`da?`$9ڂFF+_q(.1ČTIjDH՚Im{jDdy- IFAXQ52ǚ|0&r!Dĭ7^EW5"L`7>ɪFĪLmFA"(mI`(1s@' %̍jD dnd- dn4U#\`X!H`Le!J Ax$|ޛ%|W- ,1 >ƗĄ NUN 6- UՈ>+mI`CF+ɓ(n5Q᫩3ũR,%|0r |YͪFY*p1Hm!aR5"V`jQg)!TR[$(-`bFAxU(,ImYQ5&*dC̙@LRW5"{IO՟ CYip{Pgf bFd`* ~x-#0Zb>?{YTՈ L@ps#1|J[ d쳪>7U6|+m1|iE՟ eU#ԟ!QWSb ZUA ū̊AEՈ 4J[ d̂>Ș>ȘU-UAU?˕ - aF0O%U#2|и|Ќ@aAφ|Fd }uQ5"&d@Qj >hI 1D?`dJiKǤjDҖjDdOn!bn1|>JJ[2>HeU# }ԔП=D$&!`vNՈ2>H?`rVڒc.6U#`>(m`¡0`>Hۜ`m1I 1Gr|5- -rs$PâjD$njD$%mFA⮪lKm3jDd`JڂfkMՈ2>HLAՈ2>HL&$jD̙@>J̅@qXe|0r |U# 3U# +R[: -DU`-KmjDZ^i &'U#* *FTAVNi KJ0Sj!O< < m3>Q xzR=R6-,|`?|LMFTA~ҖT1wU#b!PK$P|3{ GT7nW3b/Gџnʬ - op}916zSpצؿ.覶\ra7Զ7WRai3|SO_yj#?HVI`wO_w_~{ N}^/_Ecpm_7zoóM*,l'GNq~ZhE7[OgpzP<;)ճ6Z;=ySOcƶ?ZY c_dl£F/lxh؞sχ>1]mƼ e9=bH; sn/- Ȧ8t_;:cc۲lۊv3y_)y3뾼.qΝ{ˊkޖ_סM#65aC{aw[l>RȣS\Fjp|A:{:^ڷt~OL+&;ͽap!^+oe=7?r3m~x[ > 2h3Yalr+縶s8j'|:+Ɓg0n~?YkA;}9o>-^-koqZdqgOW>pum>ʴCmU2D:/S_LSU{J0I;I2R%}7r3?_;xMi6۱)F۪Fwr>.ڳ/2FMzL!fIܟ³!p iу1=<\omnFtӚ$_WrK#͚vmx=381|훻_c|&W$̦-XmKi֘ۛ&5w?ZkwQ߯!u~5qߞ1%uЖR^ǁ}N sI2=˔m\6_y~sNdcAZcਣIuJ}6{bv\;<Ƞv<.4D968|ڮcN; "6/,W3ߍ|jY8bu-b3UˍLX7D:q]|l9Y[`,g/]k̈́k=e?>##o0[zd==ٍC(!$_T< -lK5˜t@947X3ÍA^9ϐ_?B%ۥP{ئ|쳱=-0.[gwFhKei`{|ܿ[ڈe{fsɺ_וk/pŕmcs)"݈_Y? +x͜[%IWg@S^zJe~'3VS3u2WFX֎]/?&?JNrS1׌0bJP)k_']q+yӿũ8Wjxdj/xOs4$cEpӲR9%S +AGbNٔH̩*1l̾F$f1I 1#A'aoUb9z0ĜSQ11JVsV%KYLjdp0+)dbImČj!+ndb{+bnF!fH 1[SHG_-Ձ%&5a$&8O)tR[@>5/1`;\-ؠ3zsHe&t*m &#O},EiK3V%he!kpN -G!f\ |9C0 zgJ[>U/1Č6|gr |1(hYC켛>-A_-+m 'kbn? :HTI?ooު>9>&!PѠ$YUAj >[bF>x- 3R7As$PC& @b9D|J[ )RbҖHI 1'O𢴅!C$-G/k&tPAd 9s& k)ma +* >Țg 9> +9s>j̪4tN05(m Tu(⃬"(m`h%QYe-,):r`AjRAjQ5F6Fc-`d#jD 7؋C̘IEj >YFT:|gQFA֘ڂLR5ƊE|5֤1H 1'r|0&n*eC AOV5"v`jS52AiKcF? dm8- dmDU#J k#+mI kC  9Fce)K 1Wr%T(*mI`l,f!D |06$&Lp:s)mI FA6^iKS(1a><9>_%|?Yz)Ej >ʩQS PլjD L]>|&U#b U&B%LLRa^ڂ&S53jD gIj >ϊ%|0V&#b?`ؗ>>Kzl J[ CMՈ ރ?jS5"SiUՈLO+m1ߟC̕@Aє>ˢFd`j '$9}Tb sUA澩$6^iH$/l`/->ȼתjDb(^՟ dU$-9-FdiTb sTATAJm-6jDϤY,4UՈf05HmS5"y/FCR[A3rIƲ?>+R[A:჌uڂ2EՈX4r|G-&1\))-de|q,J[2>h(e|-c?F Ar@bA()m cU(ヌQSڒC@'ّnd|;U# SYiKT(9%9 +s? }sJ[$1A֔*`̑@qD e|e|ҖQsE/8)Ej >5U# 1U# 1|e|0grL+1r |0cF\7AT(T(ܯHm3jD*U9ヹ,de| LNFTA*FTAvNi [J0Sj!O< < m3>Q xFR=R6-,|`?|oLMFTAҖT1wU#b!PK$P|3{ :nx݂[f<{^Ə|A?ݔ4916zSp׮ؿ.?讶\r7Զ7 ]m}/z|MYޑHV/~3;e.ߟ/{m +_7ڂ٦BcVLx3׸t?o-ޣxçf,g?;.JN)?=tVVl0hn d02gg/Lmz1/q7gigA?;}nT#T> #ٴc_'7}k'Qgrn>{_϶bo|הC/fl\>oqlmږ{Y~vsk{{4bڳ+cr]gXLB2S܇ ?үqן0p6:| m~Z'`gZ18o= [nZɷO|[_kI,86z%LЕiЖIN>X`_lng[I=ǵ]kh6W;qU1N>v=t\]ä͌?L<0Yu m9nqŹkU-=>_3>]a·*ӮU}TɴqηĿ N9v9y{Y`dO"ĕ, auveJNoW~,cl*o۱)F۪Fwr>.ڳ/< oXlωa5ǛW *kdK_A ?x['qc6ݰԹC k[sl˜p?090~ρetKiܯ?ɻnbnEx>+1n{6 _S+oo|魴˯=-I+'sf?i5S#RKm%mRTtKx0v]MGs-q 7iǴq}cn䣬TX#Ve?-7l }<:<\nd>oĺ!!-cVd9oMe}<}O|^n%\-+G=2A=j>u:G39bzVP+DReQ>הAun#xY>"ܬ?fT=0֒}ʯW#Lv7_tS endstream endobj -192 0 obj +198 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 191 0 R +/Contents 197 0 R /Resources 4 0 R -/Annots [ 193 0 R 194 0 R 195 0 R 196 0 R ] +/Annots [ 199 0 R 200 0 R 201 0 R 202 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -193 0 obj +199 0 obj << /Type /Annot /Subtype /Link @@ -3001,7 +3041,7 @@ endobj >> >> endobj -194 0 obj +200 0 obj << /Type /Annot /Subtype /Link @@ -3016,7 +3056,7 @@ endobj >> >> endobj -195 0 obj +201 0 obj << /Type /Annot /Subtype /Link @@ -3031,7 +3071,7 @@ endobj >> >> endobj -196 0 obj +202 0 obj << /Type /Annot /Subtype /Link @@ -3046,93 +3086,91 @@ endobj >> >> endobj -197 0 obj +203 0 obj << /Filter /FlateDecode -/Length 5784 +/Length 6010 >> stream -x]]|_1\WX؀aCkˮ?p"+.ƌ䅴j;*Hfz_v˹_~~דْ-=".9ټYkǛo7_ϧz|*~KƤun[bjaonv3W\~p|K\I )zp_~71>owfx܍͆ø[9ق? Uv>΢ޟh0ڢæXL"N7QogQOcT cm0q_$0}"&e& &LLn1I1bwux6ɋ&ɆݔMvkwbws(.38K wM!L`&pD2-9bR ^-1qxm$`=#!0)y7ck|  -6H"p[˶XO0c |v6XVPlˎك |.3>{1g|"AD$`69|! -#s)S,} @m3(1,70zfcHTl1l>F+Vs |N>G)Z, -lrp& ~"|ήPn |{3Ԟ$\ ? >=FPDm3tK~*o/l`Ra#`¨M+Ț\ īb |F T[ !>bG:,GmೃqB}f=*>%-:P )`⠃D>g`⠃Vgr!qA+60jؠ`6A0nqA]>CN tF ATQ t& ?Ap> -t\8͕ tf,~6Kd⠃%I6`Bd6APT - ?A[P0l@mA @m t5ttf666d |Fg @(@A |; Rn_>cBtRg:-AQPQx 3nHa6W-:H#Axb܂6Y?Pmda܂lB:Z-(F`Cq Z t91P0l@]B @msK >֐:L a x YXwC0øC]I5G ?!_ A`Ȳ@kK: E- - tXY$G$Jf=",72n[7>g?> -ֶ( t0)lPP\PtstsYHDactP (jM,Zf=" uJ UtYHDV-4@F-Ĝ9# LXHL:(X#@Y%xݶ)@zDfdG:#1ou tPp"-A:b,@kǜr tvG$A:Pn"։4$ ?A SnJҨ |NX1@)-:8#A11nA#Qb2-m -g=",;am"FqK"F"t1*[{v5OF`#B1~zD:{R&F-}QFgv8 |v@=tc3[@5tc+[*A lѣ`؀ E 6G3nA-G;sFr t0BqYU(lRR -E |rGI(@Sf=–#t0& d= 5tsGQzr t0Y -Y9Bc.r ta.G8eM G8Wb=J0n6M jA @w< vnyt3tv"l3t0dcܒYa=3t-5Ɂt#zD :XR66ɣ`69lPVn-phƵUeи?m6}v˲@u7vTo|=ק`OR¿Xͦ5,:U^Or`z؇tؿϱ&g? x]+ͿO۸~lXirkfoQo?Oy9S}g[?iʩ1|Rvm+|9k *F*pRN.QU4; 7][3 D1IO sϣj ZꀸApLD 89|jŽ 㘪O=Cp$~ܑt X=c.t<,lݙAO4a ,XD duW ,dN_{Ʉd 1PTq^>8wf\* ks-:q (Cy!M@1K 4Z 񴹤Ơd;fYeKW~Λ`6u,,i[|zL#|"D؜m1kaV@fUzPgN_Ⱥ>s?C9Ոѫca}9b3X-#]DԚjXsֵUzdKiaˡ}@űJGJqkv`~(NM髭v7Vq`ٱ۽;o,2f!0LUAՋi+6zuomZ.8޾h3\.nd:A>m¢7ŪQ"ȋws겤}/Mkesu[3+>v~ux'rLZ}b42k1Tw] ק e yu)Oulkg˳1:yyB&fuuta#%q,.Ce.xoO֎U=9&z?nۺTkViu[۟]Z} ͽW: JH߹NXگuİ֕26.0ڹӽl^ /i36OݼdIZo؇c.s-Ф o?=X=O:Et7R\P\WDMo3fuJ1CI|k`+nՙnkȷ &p )ԭ)hOK݁&vw\B ?AɪMp#5UEBUծ5Yz9>\_.w (L3->}KLkq6tk^PV8v98q6 oT~^*2ꅫ&1]sB ?@n޿P?or)6 ?&ƚ#WѩW q>tڃM8s:/f몞kSǼP+ wV}=N3PԽzoNuZuFV9}'_u]0Atndfjh7Ew -j{ -50ֶD?E8HC+/^IT`t'r|W@W -5 zL12ri3K:i<@sVێSz?I}g>a˷ssiv3%L&w0)ţWxP|rNἵd:/ւ2-i-7J֊:}95UHo Q~ߦѫwyZ-& <ƼYV~^#|sb*V7j-xZg!T ӯ'5-Q -^l9/|^+M -Cfh`umj|]$\'*hׯ$hMT[ -=WjF]s1L9MeNHnYA.jB@y[kA^Aj.29^_M•&ќ6Ds鎺U[PaL-8-- -W+&iMa7.$cټ 8;a7T<-2K:u^6ôEx՛I~IJ9=t<"ō<ԑi>gO9Sۿ\gfR!~S170ޕ&vJY]:,?<[Y#l ,GJ:˸a/MZE_ڲv}q2V!3gg[GG $0);4 ]yhk?iQ0ZiטZr}1tYǷz w]?Agz'74(_!zS\ ]UHS5)v~h5b+ -ԚBȗ7wj-od2CM]?ۍ$՞~z=?7'Afnk`oպ8ߞK⺏>z%X[! ƣ+FvU7VՇ;~.تWzcS_}N}z݃kl?祇碝l[Ӣ:/_6ms L 2ԁVZ )4 hqDZCtSxc){KccG5cHJ\du 1np,7ǍϺ'nʩږvw5jcld +x]]$}_ l6f{ke!Ut\Ted*::'RUG~isI1<%1[ /?|Rc [lެ5M7 ޛzC?%cRN:-1KWv374tk/D{ A6LmP"3o- q_w̖= ƹ A m$Ffs.oU ~My]]{e Tm`NP]Ϸ>w=2"CLnbwsr\gi.ޏ `3%zGnbL60I˔;\`6eHZxjˌF oS |'lJdLb|xjCpz02>2ςgY>Oyc9B |g69HqF3Tъ~d%S| ?SY +lrp& ~"|ήPn |{3Ԟ$\ ? >=FPDm3tK~2o/l`Ra#`M+/ ?\ īb |F T[ !>#bG-Gmೃ qB}f=2>%-:P )`⠃D>g`⠃Vgr!qA+6k `6A0nqA]>CN tF AdQ t& ?Ap> +to'-:hse)bKk8EJR |.~ tt A6qshe:a:tf666d |l ?AQn%P w ?Aܒk&Lm3$1l@/E |~{t99;<6Kj~e⡃. |`?:O[bؠc>K ,[ YACA˸lPx a܂+lVfĜG |N6.!69%teYiCՆMv[;:- }g:-` `60ls9XQoϸ%BᷰQ:[-e5"dIr t0J)FɱQ'OՈ"t0FL+|N6RjD:r sFhOX(B^`gV#&yVKD E e@`% X(A6[ rjalnG6= ]b5dIv:1nI䬰Q™ :˸I lr`5Odܒ)F;a9t0y:39܂Eӻ͸sh>m6}}p.n~?OcBُRXΦ5[L~Fp^^ϰ}3o~|Vܟvܓgݞ_#|Eqok[|~ڛ;|57?{ͧzOkkͮ-f~z9S}q[iʩ>(T{g?Yड़\!h;0;/} @v^k?݀ߓT g:`=ۃRAzhѯO/=@v3V5`ϷtPvA +ݗ>U~ + u +3YPz1i9ӂ4`{sYfݳ@(}_AY͜mk}T,ҙ8<N<'Xc5@B*'@L>4gp v};(Bb@h4isIA@I_o V=8c`@w_Y5C^pz̐<ـa﷞)Zo~zԌUq_ 31u쯅si2ޫicK jO%"|=|ia~GhH%<-鴁4{YZՠѯfL~Hri|ɌCjc$+}oZGJ: EWx1CQahю{i:M'^-t +4st#婨jNĕW"U-- Y,X:RC~jPfZaSN?q)s٦ʴбԙR˨:^lmwQ^g8kp CT^fL_o9(jka8S٨*b/!IMDL쳵ʛ^MkuHU*Gz +Cw*chCpgT쥣yH:Jdt){mMDџ.aAnȈ!W29e|SF* zϲ=-U*;O*vdhmju}T||u3^?h2:IWxmB>h9}1ZWĜz<U)wLc=]|wU/phĥ^GutJ0w/'޻xF8|g֝߁E8HCK/^I`tUy[͹rNqFh_])ȷCL`\R>7#yVcYSz;Ivs~85mzi=Oki>S´ܤׂH*^֏z*ӲlF%Zٌ{ʹx 6IS hUWcʕG}"qk#~Fk,v3T?$%rIFeĔ8O/:DMm;:u=wI0x?pV״D3p+Mrv!e@Vo1V*w.|7#XF[>ö| \'*hKL$hMT +=gjEݮ9[6:sjXfa K TSt ˜uV y7y5R{\}ayMz7+ARV$ bdPtaL-8-- +WsgڳENҊFI[uCrAV!SyS~vn)y܋aJ{B9' r>[}G+JU#9@6;މeqz"x5jEu-x#x ^2L^VJbMw$pj,\YbBZ{?~7@mzT +f{hm`J\sYːo?E*dtx]^|ZY##v=%2n^C8;nnm|#ƫ^%o} XN+d,_ghKs}j ͗~[hCms.yg2.%Lo65Mt#m%XAss>thqV̚cH*\'mu 1np,7QHFr=kO Zr??jAp endstream endobj -198 0 obj +204 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 197 0 R +/Contents 203 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -199 0 obj +205 0 obj << /Filter /FlateDecode -/Length 4657 +/Length 4939 >> stream -x͜]HWJ@e@ -Zb2p"_).3IXEd5}꭪S{_XWJ9Ǘ` f|)rza -R-y.z?ɏ?sS6&hsS5?Nyx[W9u?L>Tp~*m\]q:2A-+LL www.vyp0Qд-x , qY Hᦔr$Mppu%p -Xn.kH&b|R-- [3 -B%n9 2­!AB$# Ҕ"R/M lZcb -x|e2]psUb؆!bbJ !j5lTJ 1^Pbv)^bٻd:bT!f}A Ԥ:bz!i71QJ.C̑WCHS q9ō@"1ĜWbN!+QS6Na"1cJVl$lcbΞAa9'^&s!"şDU-Q,1\0{ōDWLW\{ōL8~bƷO&'E|G+1 W( &1>*G$UCLQ-ڂ ZlK 1X)m! I 1;_TffQAL+ 1{2Z!f@iĘ$C*AÙ58|F^J 1U6n0j0 - .*mq zW3vZA7A@D˜b]A,+mq-Mc>hKLJ[>hIIs%PѩDrPm%Pm%49[Ag7A40 9>e#1n/? $tR[Ad>!f)苕ڂ/H 1 YqD_jb?>Jm'b?D;H -b. RĆ &A<+ma00!昼&tAd 9s" {(m -* 7̐s&P]&b.T]I<>X.U 8-d?GU |СJ[<>j@ANVIi x@br|x|UマFT=ɱ|sU5"X@qdZU# 0MjDH՚Cڂ,JՈ<>R[A FAXV5"Ǫ|r!@@W`G&0d$U# TU# - Kᔶ|'AaAbV7Җ>ƫQIiKUՈ r`7> [Yb|{->Q}QA_C"*27iQiKtQ5rJ[>K!f>VUb |uFAUJ)R[AVFՈ>9>&U# -`bA褶0`0%3*-`d -r|0И|YU# - < R[AxU(!b?`C̉@ RU5?G|%-CUՈ">;Uf׀QՈ">r-FazViK -1\">?ҖE|0Tr h -@bD|J["7XjDd?G|*m`4QCfU`+-Q R|uJ[">ȺU# bU9슠%NE|0rhTA̩QYjDd͊|0jU(WgR@WڂFUՈf0ImYjD_R5FڂH0 XR|vFA:HmjDl)fr|{-`0Mb9s$R#SJ[><U#J 󘕶$|0FA$|0Vr #0C̕@=02GAiKjD dҖNՈ> 9O!H 7FAU96%-\S%|09딶$|0VPbv^dlFi r r F$⃌*mAUA0 PìjD $njD $n%U(]T9)z+L8Yj >bFAbrFAbR[Ab*F3Ĝ7UQS&PϩQ/Q/KmjD*U9ჩ$d%| L -FAʪFANFi GJ0Sj!O< < m3>.Q.B̤z̤lJ[2>FA)A~0%Q?IiKF(nٓ( >I{U`Wg J_8R]o궦/h 8]+@#Ͽr ܾ3_6`{}c9Yn?=/->U7X=1u;+sُ O{|6,a -\y31=ً;|j.ˎ!v}=D\gs?>iFh۵izh˂Iq߯ߴ1:|k{n~޺\mg&om6v_r}uõv9ccc 8{xy~]_cs>'22xss2=:<#V7v4"qaݠ`^^ݰ8=Kv,bY䑬q#:>#)C|f[쵝q Ҷ{lqi -o[um s9i# f:̛eX@qW}pnlr}&iV0 xM`E~m]|OmM]gTQ6n_W_n%F5i|pI7"d^eI=Y;qݷwLJ[o.1L [%;ϑӝanǝvMn|/#j1DKWOw|˒o~WsY2kLVѹ'fKV˶=Ƃ -knww|`rG_O!Y`/u&Qn8^i[`gGz4籨Iha:V{͈^\N<pNX@ƻvX̧`_?c>buɅ}O Od3Is0ǾM`ɡ=)^= XTʼ0u[~]ծ/EZ,S0Ջ}Ӟpsu+N]53X0^0(=;Jg ǝvx8|b>:ݑj챾0ĵnp̞>zssBevu qn ]?wKXwܓ5UW3.xQAK?Vcѕy=t`v3OgfgY2 C?-?ìMfs >G~(5e B~1=Gin9텞utmU<œ|]۞B-6A3ra {iz`S*c_|sW=2?<嗻"ei_^~\}r:So{y7}Wj#_{;e&ھخPjѱ:#dni]l&}UWDؾBari 8$w5ŗlK4V`r%2ki߽k+="6W -{#Aw+Xn +x͝kʕW9?1! Blw;/$!QI%iݹ4oH[U^kזtb mO v*_~냙r4-v>OFc S(Ŗm5s)M~ׇ?>C>W?ecrɏ:7E[S>od7:úC=]۟Bjt8lC+? +7kRrϥwfhg}؁ I1ulCpNz|)*2CEsd6<>Gg()|FdXe1ze)dE*6s)ψrdKRUB/ؔMG +? [HO >^a#sAM+d|Fg?K|pTdbJ Whd[0iZOiK[@>U+m6 +??[Ami$V[ډ6,'m!?2Y8t=C'p 6(nq JAp -Ҧ~:h#9Ɔ@٠]TAXiiUAm"P@9Am&PA2g > +? \8tЖRm:hKL[:hIIJS!)tA@a@ڤu, +?3 + D8+ 9:e#mh/ +? t[Ah>iϐSA+_|6̂d t~Q'PAQr :<9gO 9wx-R3W-t60BYq S 6?66%*n ^( Af*n!@s" 1o}*tg >gr teriυ ++>ҐG˥jCl:)NqG稪C$QAţf +Ddgē7&aCAqGլjDl0:I% +l6AЪGàjD$k؄,yt Uk'Qr :ز(U# 1f%4TȣXV5"cUr :9@@W +`[&l6:H$U#b׉MFA)n O0A}Y՞:Hl-$6tH[:HlTU#\ؐ5ņ@`# 9O@WApU((n 蠯 +~A_D tWH7iQqK@tQ5[:GiD+(lթQ@kTg6S+HnAXU# +`p + T(UA܂ jD삱)Y՟IQInAC T6`+GInAQ@U5 gYՈ:"RH m9(!!_ + F9JzA0Dt UU# T&GU#`ȵ |X-ѶH@B AQDtEU(lSl >Wr^qKDYjDd?Gt*n`4QͪVZV5@6 +? Dtu-FAŪsD%lNEt0iTA̩QDYjDd͊t0jU(WgRHW܂FUՈfl܂QՈ =jD4$@٠$cI՟#:*[AکQD d4S +? C[A̖HlL)nI`%tẙ[:aU#J [FWb`q1t0Vriϕ@=02GAqKBjD d▄NՈ: 9OB!H 7FAU&%- dUՈ:u[:\p(|v^dlFq t016 >{rtU- +6L@a %t%t▄wT5wQ&36̲KnASUՈ:ONՈ:OQr :OEՈ:9|39:JU#J` +1L m(% vV5"U9$dq`\ؤjDpV56[RbC m +6?ؐ'݄< m3:]V56B̤z̤l[2:FA)A~lJP5dtjDl 9:=9A3i?sras 솷.*ݿqnkۖ^uEgw07=1pÇV㇁;E4wQṅTr;l@o/~?ߦß>^ 7xŘg>~ڿv.9 frM(׸|]Z>7>?l׶6c~_nm69 /7f3:nkgmeϏw"do(j' `۱y7'g~G0]Oz='knshAwmm|ͯ xyY̓-mǽ8_v}.,sڌn`?8uAhF`_Aakفdd1wlA0>!x ,3߿TƧ)g}lX>3Lk.10n>`¸@CGʸ:ǝtss21r[{G4qb7'#:";zqh/z<'vjָT+O@;۔k [Aڂi8ƤBqӝUǤ&ls]0oFw(cT}pntM"9,f'vg:t0p";o{-.8y7Y|$^LeT5.?%i^2ܺ 8g:`T4wr3F׷KM<>=Ҁc² ʛ8gAϏ3jnʷ =\W3[ ni +6aGĿU|3a;K@FZ^ν綷].<wWB'xv갿[<s o/w:٪'1#.+#Cz٥]h](U^M{*ڱLGɷ "%۩VD>ƍ^:P;L1[H8V1eâ~K pDݜJ/dEض2v) +k`5(eo0F. C-=띯ߪKٷ'J~w7؎3!5J9:k32ld/ y\y;ӠՌYG⬣\؟|4֥a҆T<]Tٝq.}L^/ Ώ p܀ #ZwH:۷-ԺI-Q|E:zڻ vߝo\;}̷]cA +[)+;NH0c)fƜ\Z5kDw?hLb4|,|G?.;[t ْ&zmaKPǂfgܵVzABƽh^uFQn؆Ͷ,B g[y籨Iha:V{>zS>Ӌ)C @dTQo@og5~|2g/z]rgEؾ'څ`Z<7 {p5(bXTrwӶmjW_"`%cjz1O|nnn7Ů+82w_ԋƑrB;l}H'1#X^)pH::ȷ徚9 {/ ӷVЇWxN#l^.Xfwk17o׻$dMgË +/1rXW{> w3wgbg5s_aV"_ln{eǸtnU<œ|]ͦNֱS4#w/ OO]͹uU7OU7S,_#|W7]Q7~ww~xﻒ}_[?}i|K &ھe|v}{?[cᱱ:}ni]uo&}vkҾ%ҾX=$wElK4R`rI4JvPzywUž9ڶMl[?u endstream endobj -200 0 obj +206 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 199 0 R +/Contents 205 0 R /Resources 4 0 R -/Annots [ 201 0 R ] +/Annots [ 207 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -201 0 obj +207 0 obj << /Type /Annot /Subtype /Link -/Rect [ 37.466457 331.353222 37.466457 318.950022 ] +/Rect [ 37.466457 278.639622 37.466457 266.236422 ] /BS << /W 0 >> @@ -3143,33 +3181,36 @@ endobj >> >> endobj -202 0 obj +208 0 obj << /Filter /FlateDecode -/Length 1607 +/Length 1604 >> stream -xYMo6 ϯ a)Z=n?PI%JL &زD>>O6 ~8s[˗ 0!͗tS|Hˬš+O;9NJɴKz,uv\7N|_?_>̜ږ3EMӧELK%9<ͧэrwƒLt0_&*ln)̳^udVrX5xS% .aN*إ3^ 5=~~8q`Rz!?d4t* it0+R]y} 5_iv-~Y`-ZGpŵ IصdKS3a(x@v3"@4DQ0P.u͋⹖8M($F⒱)=RBÀdHWqIG{`^*ہJG"$@b3m ۍ&UL vf\B|!IlŚo3pb4wL"L֎HZb؉reȥf,eUɮu2s-ޒP}sŢf -1cB(l7jF! -؃@)_]JE;;()pvbY0me(G;:i* .u8!: ^Td'sOvufy."C&v%iU;?}&`/Օ`d$\w IrH>ci}( EF/JiY6 4WP45VTJ,waBnNrꧩ +*ߑiun?ʦ462`*{6&kH#۫l pKߏ7?Z1%Ša79vc Tߴ<=AQBvв) mڧFF bLϰZ8g YMQfR! UUnK^c 6Q5}S*, -I3J@T9I˵Q I?saݦ]Q;V#i(#?dQ)GJ)Eš} '6\4mT24/rtP^o~}L -Lq*rL G qH^ˈnK\^Ap -Q6j|LKY!"ݹIC\45J1M4^(FzHü"5h9x gtrdCJV(PA o|bѽO)В>k)78qބ+Xäw&>g2+:uuS~;ݠXF,b`)#w;&bx.<Ev$|akf[4,D0c&-dId]<:wQ?cm +xYn6 ߯TC% Ȣhv3@(ur]L^(Yv^&,)Μ֨ej?iDw>_.2뵰f)qBR2|}v^2 `]8iq_ϗ+g|r گaQiQ@Pw|NU a:L: ,jȮt6px'N"x!}% + dlH0 45rfQg B5=aIΌKo$>5I[)[>w8lC!#% v"ph90rkKYU?/-6g>[OquXL8C!ƳbwLe Bm(dT{:2PiWFz(J +nXLgYce 6wt4@]@[ qB @NZ簟] +E&LJXWӠM kvT?5L^*I<;$ɱ3wT8)*w-ᢱ荀B]--[R*MkMEؼNJS*X}HYO$B|&LMz`w6'ꧩZ ++*ߑZ~/]ip}tk ,eѳMb#혋M֐?(+GrwTYuΏ7?Z1%]Ťa79(vc^iz8;z F 9Cj<珤$Gk"Ff>A1.;u4'i8pf"]-:y!=8!zs=$!BkC$2v@lRa[ƦT7Xȓfhrk7_gv㳩~.9!Mݻv3FQFyv1Rj90y>f4c=('w +'TЂdXtS=O2?>$f' ܛ}D_k!т;L|uU\X7oZr.1o2bxc⏭̃!\GB/[ff&EB shAt'QXFwE)B endstream endobj -203 0 obj +209 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 202 0 R +/Contents 208 0 R /Resources 4 0 R -/Annots [ 204 0 R 205 0 R ] +/Annots [ 210 0 R 211 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -204 0 obj +210 0 obj << /Type /Annot /Subtype /Link @@ -3184,7 +3225,7 @@ endobj >> >> endobj -205 0 obj +211 0 obj << /Type /Annot /Subtype /Link @@ -3199,10 +3240,10 @@ endobj >> >> endobj -206 0 obj +212 0 obj << /Filter /FlateDecode -/Length 4141 +/Length 4164 >> stream x[#Wڼ2`ca?tlfeJ*bzqOiI<qts5Vr:=ホ9͈͋xTO-MV_۩:yB'K1r?_Zs!L[~7~rᅆM]J̇mO采Ԧf`ok,006=6w̷)y@zAɦ?=۔bMWs9cDO Ob HM-IU!ܜsiǐLn-т-VncH!bV­­$ [Kp @@ -3220,22 +3261,22 @@ P /\Ot|fwL-Xp?xsA?Y*fm&Pim^'r3y/s#>3$כ90b]]fֶUv|eaRv.?7}:^iev,Cۘg̚Ebu <>vϮ+kK g,v{kZ=g`E%k.L;c`۸ɚ0߇rY60hkq ֏yhRsq{L [eFy9J[=vu4,Zpqa=\lM1d~?m㽤e0cb_1K aƘ8]hXqf[|4&_kn>_G_pX hr^^#lw69E XiWXGJ,8 RՆ||D͑}Vږ=Aޙnl_^O -a.+&/1t9~ n`̶9>S]'2Ul7)~ڲ/^  ꮅ,Ŏ%CɩcWڱĤ`Hgo7￾Ru/u8Su%|u~%1V#gѷqFV\gn45Qpe1/y>;LJ^5<_Jmc _lcO9?%6cp779<>j)>VwX{}+OO_V`h7=zM .xKjj٩?~J>YcY59oN$omֿYpx$3K><~qaNt; MtN;yKllP.- }:m'6~/I{ u;UQYDzv/s%?k?2}<K֪x?ߓ|[g4}4e5[u߽M[T߻m[޽M1[T޻m[ߔ޽;[nL߽͎KZ__[7OƿoήR}>cmoB\WL+{ܰdǝeUx7Ek+`%!llz<'xw_#ܬ_Wwl96kP÷1F\rJv.do7x +a.+&/1t9~ n`̶9>S]'2Ul7)~ڲ/^  ꮅ,Ŏ%CɩcWڱĤ`Hgo7￾Ru/u8Su%|u~%1V#gѷqFV\gn45Qpe1/y>;LJ^5<_Jmc _lcO9?%6cp779<>j)>VwX{}+OO_V`h7=zM .x!{j{D賺^LKI mGw>,ZU߿oN$8!}-7XDY?7|qiׇ֯e>|`O;b飳piGa;{g2:o--hcZ vο7壣%Kιj\Fc:rn}xt/{?kU֟_}Y3wTN?yWoo*$ojwhoPoJfޭo7fg%/{-s'7wgwTr>]Mp67. gk?txnXj+ Z[e<ԛX|uzޕڿ0}’r[6PcwX_ݻ\n/<{p~M5|#.Fx^|lF" endstream endobj -207 0 obj +213 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 206 0 R +/Contents 212 0 R /Resources 4 0 R -/Annots [ 208 0 R 209 0 R 210 0 R ] +/Annots [ 214 0 R 215 0 R 216 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -208 0 obj +214 0 obj << /Type /Annot /Subtype /Link @@ -3250,7 +3291,7 @@ endobj >> >> endobj -209 0 obj +215 0 obj << /Type /Annot /Subtype /Link @@ -3265,7 +3306,7 @@ endobj >> >> endobj -210 0 obj +216 0 obj << /Type /Annot /Subtype /Link @@ -3280,7 +3321,7 @@ endobj >> >> endobj -211 0 obj +217 0 obj << /Filter /FlateDecode /Length 8283 @@ -3312,18 +3353,18 @@ B=q nw.8퇧?aﰮF%N ?](ZdUn#{ߙDb{y'{I{?{?"{=S{={Ǒi{={{}{}EG]]Zxs =$+ݷg.RkHZ_x~bxƧV2E^c)۷)w]( :ZsK߽7.˯Ed[2K"&nB6zk0rv9u!B6fB endstream endobj -212 0 obj +218 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 211 0 R +/Contents 217 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -213 0 obj +219 0 obj << /Filter /FlateDecode /Length 2986 @@ -3342,18 +3383,18 @@ Ff ,! endstream endobj -214 0 obj +220 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 213 0 R +/Contents 219 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -215 0 obj +221 0 obj << /Filter /FlateDecode /Length 3551 @@ -3377,18 +3418,18 @@ o ~y"\r$=8> endobj -217 0 obj +223 0 obj << /Filter /FlateDecode /Length 8183 @@ -3408,66 +3449,69 @@ x ߻}{N9Ȩ}눌}UtE&uLF4]+>9q3gG7B?WS* JKh-ԥ>+}xYY΋*\|߃9g]Db}@L{ `Ӭg=]COݸtn8Z5{VSC6>c^!q4;{k\+˔WiN\/n'_c3/]x=H|yX1siT bUS[Z fa*ص>5k8:͙yT|ۧ5$OxL 0)]i6̟?CE%Wű=Eo)-3F/죙b1K5-M+޽y66˴dր+-16KK ӯwƦ[ ʇ7obpza~0_ً+{X^WƛTf4?(G{^.>ZG7|gZn߈x,..ξ7BbmAow_Uuc}մ5|gr3O߽~]7+p\nh:,4n/:ch"kc^Lg_ɫoKx<_]m lr؞qN6KkNY7.Νo<w[ mosS<57Cw#oڂgBq~HJ};Hb鿉_E޿V7| /qO?:.@Sq%'W#[suߏX/CPVOORj?NO)ד'r~|/o@7quoTOE)UPޯbWvg endstream endobj -218 0 obj +224 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 217 0 R +/Contents 223 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -219 0 obj +225 0 obj << /Filter /FlateDecode -/Length 1465 +/Length 1487 >> stream -xXɎ6WBs+.1e  An988H"%ؓtcPz$~S/X%Bi|yHA'NG{/|!'PJj`~rz뒞/~RJk*i˧˻Y 9}粢MU(" J]RЀ -)Ul5=aOx0_jVn3*3{y5w=};+;#|TlDiE1j姟㤤0ƥ?yczD;pnmu7y5&L+D)m[Wytmݮʦ&'9C4EڵJ \<:C1Ud'yyt SύɎK aeѿ6@!I6S|V%=sk2s 'ّslN faI/{vXl[nze۰>@rtTR4 bc œ&9%2[ɪ,hd`(($ʚ*C1B}r[%t6_eF`uvsȶZWaekIj$ɡlAѹcIsZSa:3:Zc4CxLZ*t< ,adKB꒥@ryJ9w*lyx:ƉV_5N^+ * -(uHAɾkF뗦VZ;u{!l˿[~x >z2=yH f~Q7@\˺ʃo&@ǎJm" PTV |SCCew6ݠgoO$ ts={k'zHhg#>G6eYk1%vT -Vg-Yz]+UHh]j 1JXLϫqDɤ,)+s;vI^V&&|Z'#Gd^OfQ-azْd!`TUkC\7̡4]~^VX:Αt^PA|7;8UϷ#G,zHddhpڥ6BO1Ѩ}f7h IJ'|l镆9"<}%VjP>FJ+[n\t|=藷~J!!:/r?Vsjji^*7ĴnVTWA(%H?}_~sIh?)]BMì>wYQѦ}m.ɌYRh@lΰs<ݯqNgo7 o˼~7+;#|TlDiE1j_~Ӥ0ƥ??>yszD(#sF ցD՘0* Tu;xk +\Yu>X2͌<t$Nc9B8]33 nD%nN@1 ;;篛Pxna[#]a[g)uiFY=Mلy{81 ){&os].2kx?qIsSR7.}a9) +,-ߔgD~7y%GErCF*᛭qy=Y⳥.র#تl3W$hRdƃkSa1Qš`ZҎX(KLk"ؕ9w$/f;>M3ǡ`#ܓTc8h^h$Y|VE*n{m xhU] 0?`dyuм,:Ζ׆v^A|FV$ovqsg+gAE9b""ENv,CS>/oEZ$:{v#@,}GH|A@.1U +i~$x_݆;~y;x^Q <#)D6Eg;Ny[Q2vwtn.y@ة6fwsA7}/BA0-ST'pi2TwF$C5 2j.eXhsnpmy^Er ֍0]܌rƹ™Kdfzx(w}(#QHa| (+CN3Jy;X endstream endobj -220 0 obj +226 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 219 0 R +/Contents 225 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -221 0 obj +227 0 obj << /Filter /FlateDecode /Length 2389 >> stream -xZۮ,7}ik P@BAxa"J r/焜ŗryUժg^ }pVO?8sEsN1t4 Nx&*TwySbsY)zZsӧ`O86m26!b߼-rf6L)#>x?w6ލ7mZ7Ε܈xCtkZcnkmy&ƗzKש-nv:woH2y/mtW:V)LX+L}3L߁`lhATPIo#%с-28uA6iPivO4hӏ8{f~ݳ*a Ɵܺ.r]pkao~[]8?G=N*XM}6!I&c4|H[E :nE)!rk36{sf8k5ʨ] "NqsDfp߅{ƧB߅dj:(EuO=egOqI8.:̾q)I3Ҟ;N {nJ1ktCt[L)\ec+@@2~]pEw]iȯ ]YJ]@\f ܣ>GtQfǫVXPGϪr^y8E,+풁0 -`%:zb?+AZbvF^E^c,ͦwjidȔTt|u7|;ŋ=4q ND %|_!3;b]GHFa /Q}3±H}):I?cڔ+oy{AEx襎iÐZ PVƲkU~}cH~&s界*Ti􉸘>aGN>>:}bOnS u.h.m[A=)a,[^o#Z5T,ʎi@1/F ҃{F1ugmqz{VWO 2Ǜ#&hHz^0L(D҇^A.$AOd\kҀ - -MJ6zD 0r(5,97Q@|Nު[(')d= ;AIxJCmR.={i72;({, E&zkRKd>@kGA]uK FO]TɵebP&Hft00W:(ᰠHG2hX,Ř׀;lG:"jpb (E6\pXL0Ĥcqef_T`OC[ J -쨤Tiuc"W+H /y^|iD8Y$Dvo˷~rY a2J~'kYGO-#K"8l? J+9δ3J8}\NGy}$巸M5mȌWE*7E}:ςj韫 6 u/! &RgIH;$].oӮzjxyŹ%'%) +xZۮ+ }W0)@biQ3>E*iD͒FIsv{.)j\8Z0xę3_gI~?s%97"P;}ұ}5v=ږkO2^oc8˯۱0GA>N9IN=] +*XX,zpw2oA>Z{e<ܼ%-c[<ԫp]i:GhJ~8f2]q~vHPI*67ze60lWo@F[{TM߫|L;QAn\˘P5Ei]^aQd4ݗ Rϩrn)r֢kA +sT\Y. is\0R(B@pIjH29b҆B rOBGu @NYm\X.۱'U#bn,k*Bb{#\"e&"6i ]x=Ne)goYs;x/6=/pƅbz'd/BV).h1JanQ9O`:ҟgjsEH&),m13 L"L=Ѡ>< Ϧ98No/ud^F] {.< wR2o7 I3CUJj8q6E۔ٍR]?"}FQ2Rɜ𯜝%;fq>Cm07ASohui3W9΅]:7* 4ԪSz~%Z ݕAZPX}$$@k$p\8Gk}W񠎪UrreW^` JUq~-^ }*y\^M*צӼRŭ5ѓh"wB13D#Fi <~<cv#"9-5zcnWU(ͧgs#CC@Q '; 0%HA(5ha0L/z]hJD5o.!ATw?4Jd; yS@xX1mʕ7"{'OXĭ[2Bkȝ O$> endobj -223 0 obj +229 0 obj << /Type /Annot /Subtype /Link @@ -3482,7 +3526,7 @@ endobj >> >> endobj -224 0 obj +230 0 obj << /Type /Annot /Subtype /Link @@ -3497,7 +3541,7 @@ endobj >> >> endobj -225 0 obj +231 0 obj << /Type /Annot /Subtype /Link @@ -3512,7 +3556,7 @@ endobj >> >> endobj -226 0 obj +232 0 obj << /Filter /FlateDecode /Length 3298 @@ -3533,19 +3577,19 @@ P] ^׺!vq:p|H H(w}2!Rl& S~g endstream endobj -227 0 obj +233 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 226 0 R +/Contents 232 0 R /Resources 4 0 R -/Annots [ 228 0 R 229 0 R 230 0 R ] +/Annots [ 234 0 R 235 0 R 236 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -228 0 obj +234 0 obj << /Type /Annot /Subtype /Link @@ -3560,7 +3604,7 @@ endobj >> >> endobj -229 0 obj +235 0 obj << /Type /Annot /Subtype /Link @@ -3575,7 +3619,7 @@ endobj >> >> endobj -230 0 obj +236 0 obj << /Type /Annot /Subtype /Link @@ -3590,36 +3634,34 @@ endobj >> >> endobj -231 0 obj +237 0 obj << /Filter /FlateDecode -/Length 2771 +/Length 2766 >> stream x[Y~ׯ06f'AXA- @lYd-y$],-qN^ }pVOΜ/hsHgA3gBp`P)fRy=CλR2#&χgѺ<5 XXʑ)eO?A|=y5{ _K u,3^^y3@ϸ<2m~}LayL_3-Ey㉿}suYyNr2+.ӊ<$Zf]3LUy4ɼ>i5|>DK_:\+~WYHdQ]QΟtz0nb(dĒf.f6G DYx]naFae%O SWڨĿUhPPf%UAt!öHHi\CNB @Vy =7\U1efHLfJg2X0+1lR8\XdX~Z.^׳:-"^%z]^9 \o -yUGi^U'4%=H LZ[*ty1s\ -“N;6I9ҋ GPF z2ss5dsԜ.-7Q ~bJf@D.π")xL.t;G.T헳 INc3~Gi@d!!\FJI&9zЍr*Lεk04`57gsUwD{*DLjdo.-,vX7?lC~:&sR!ީ3r:Of2cOR|Jn:F>;Ԧ$&B0E/әˌ9a tO3GZ2VA@]Qy&߸E!XBop扅> g9-Ѐy) H!jscVWK-JnDX -(> mho nq0Kܦ@1[POJ|r6'o%.߮7sYY(<("tIܥ!x`G0=&1T1IRMeF(&&A_vЦqüKY*wLbI{ ti j bEa"s-^"v?CbD 5=$a`*Ԩs 2m+,Xf!ܩإRYrz X 4pޤϳDGOym׋&›Cyq(b/4^7(L-Uj zEmsqoN]o ͱG#BAH>ws}UJ-̫/0jkJ˂=-H/g:uQ hVڣƦl BўYgzJHѬkrCj^5ıڍuVF}:p\ua[6C r j]1u{84}d{{X{0>KI;N(#ɝlpb+d.47HHȰD$uI0ue~ eO&I'): I0xumI|^s[#1U]̝}. -6 L:q)k_nEm}7=R<`$#$dV'h?V(_$4))qW_,ި -P7.q{Ĕ@K^B)I{U,̲/TI1Eם,nWKQ|"p.Կ/ Rk -P_:,Ǚp2FUSqќ6cB&5f sʵL`Xn4t._Jf_Jp[r%9wL+z!=nÍwjb;^]vX*9@MH'PӃvP;|G[A%!4[z ހ=0g! M=g! MI=g mA:g! M 9g!! M8g,4%ޜ۱Ys,栮HO=BSCzC6~(l4>h}6nѦlsy7iu tpZ2)qxtf<:WCD|3c2A ( ǜ5q Þi"^ȑǃV'ƒ=B%G ^Ӻ2e_<YF~Y0IgL3) - D !AGjv V+ <) Kd$, oDRKMjaiHEU +Mw9XU͵.__rP Z֦VUXD6s~a4ExY_(2FJX{fnr(l@i͇=ZSPaIa4`W__far)rcH :Ghu{`h+k+l?WI/2>}_9L#Cde#Srʍ';QN.ecX[~ +yUGi^U'4[0G ԄqJ--|-ΜTjRu|j;^d*Π7Jk؉@A>2*9W'[=,3riѽ,S2; $w~-ITf0vsG9kjN"i[T;"L 27I:F/QkS fr%_8˜ + "sI!:F $t+8iaw +_ya ҝ0!tz%p*z?Ɍ=I9);MfPGLg.3`5#=aAk 2[I?R?tE6BHc u  $@N$٧@ p&y .)DZ씤ܴQ} 8$%*<a*M2o7cᭉ(.Zm2NbK\5V']o粲Q6xQE>KsC$az4?Mb@9Tv& bZK遍PL:MʝM&5T6 H{#)/d )'Y&'Ċ6 +DFYE,~$Nň kl{=1HA!yU^QdV=YzC>I- S űKub74H hIg M)=NEŚ@^ Q[m>qn4M>VIk~R9hD#H QnoZy$FmYiYpo%ie>V:j^;a6J{ؔ D(3 AOW)u@aWk8]ʨCGΗ4L|}!rPNyA\ 3n&ϙ qO~ktgi5iGtZFvOd4}[[O t FF%$KR R_ǜ/)t}2O=MgHM"˘nKz41IBWbvTѷn0gyx0KYjt+-j뛸9mwvo-#q!a%=D@> IIþb9GF W⍎ws#ėZ:LIZ˜bYe}JL)dqZ>{{w}y\Z3P:dY?􆃯1+0t~0kSVeM4r.vⲾPT5WW[ݒ#7.acZ;5 qntO[U (扇'=ʦw5 у Mk"GBk vہW78Ra n~"(˸hQu~*i,w=1AwPI ,x|c'^t7`iY0oBStYoBSRYpoB[YoBSBYoBSY8! M7gv,E֜& 9+sД$Фސ&g6J~9Mmr6yhl)>3\ލ`ڭtcA??LJca\1N8ߌh遌oe0Jeu1gM {°gZuHr$:`Udf'kɑ>6u?"DLW !AAV_=p,7SL +$A(Hl4GxZ݂Պ'wGH!.OJBR++I: сԣRZXRQtUEz:7JӝtN"VCs-%CgB'UV(M?_X-d7^֗xy-+<{j֞Y*)0JxZqafTXRh` XUחY\RBw/8r04pڊ.0ϕri̟kr`a{AȔr#0__ wY~}`6x endstream endobj -232 0 obj +238 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 231 0 R +/Contents 237 0 R /Resources 4 0 R -/Annots [ 233 0 R 234 0 R 235 0 R ] +/Annots [ 239 0 R 240 0 R 241 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -233 0 obj +239 0 obj << /Type /Annot /Subtype /Link @@ -3634,7 +3676,7 @@ endobj >> >> endobj -234 0 obj +240 0 obj << /Type /Annot /Subtype /Link @@ -3649,7 +3691,7 @@ endobj >> >> endobj -235 0 obj +241 0 obj << /Type /Annot /Subtype /Link @@ -3664,7 +3706,7 @@ endobj >> >> endobj -236 0 obj +242 0 obj << /Filter /FlateDecode /Length 2931 @@ -3687,19 +3729,19 @@ u Tq+1|Jl".zE!<e/RH"/PCba6῾,L endstream endobj -237 0 obj +243 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 236 0 R +/Contents 242 0 R /Resources 4 0 R -/Annots [ 238 0 R ] +/Annots [ 244 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -238 0 obj +244 0 obj << /Type /Annot /Subtype /Link @@ -3714,7 +3756,7 @@ endobj >> >> endobj -239 0 obj +245 0 obj << /Filter /FlateDecode /Length 646 @@ -3726,19 +3768,19 @@ F 3Ҧ$dWSciM(=m#+2Kj'+:}s"P{W*\l9韉@:8.Z_jWP˗^I?{ endstream endobj -240 0 obj +246 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 239 0 R +/Contents 245 0 R /Resources 4 0 R -/Annots [ 241 0 R 242 0 R ] +/Annots [ 247 0 R 248 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -241 0 obj +247 0 obj << /Type /Annot /Subtype /Link @@ -3753,7 +3795,7 @@ endobj >> >> endobj -242 0 obj +248 0 obj << /Type /Annot /Subtype /Link @@ -3768,32 +3810,35 @@ endobj >> >> endobj -243 0 obj +249 0 obj << /Filter /FlateDecode -/Length 1859 +/Length 1850 >> stream -xZK6WI`!%-P qC[$@C+Y=Pw8řs\>}='h۟$-^ kr;X $^i%%oNy%مs!8V%~rzr˗N7d3;&t U)be`T<AH>kP$z>WTތoz3VT|xIť7i^\i(~~p`R`'<./ #P2'\4QI&~.CwX莲7jQTnD0U7<9t/|&Lq]wn}WeuqE `r,5fQ20|܀d1qjtV3nEu%ڬ|-7w$: 9 -|`<;)Uʂp?Gw BgO]ڲ,;%fx*J|65++K95eA3=I2`-@ORR2eH Q0;>}he9 t{Mfġee(ILa3WAt$#&\\?sO!B!Q\kbS-zrTa*mښXMqS6GQ›jɚ"kHCI!fGݑ88J fVÙbIdM8m_HKJ"@9P6RA"]oSܧQ4|SF?mnkx=:hhA9ϵ:(CP5'Ȱ`tW7d,۸Ey0HiExA뢿nm2)̷f㑯!5;q>ۨ1^}wVjnzul0DpX +xZK6WI`!%-P qC[$@CJ(r}S9Q0?qD#>דz-Yb9bcxSJ`oO9Xg΅`{t~ /nHh#U3;&#Q<BK2Q( y;Lt>/Y lܼzrf#ӛ0PVK5.aN +L@ϿDžN<|syHPF=*`"F+pv<$Á%=3+ǍULo63D&fba2XӄtD,VAOeAptoc@z3@2erIiy\NqΜ%9q~ Hw_{?U!62Les0&ЉEvȫ;5;.OI UfDD1|J܀d1wV2ndu%ڴ|5w${vYDwNQg!ÓWHi|C+S[e3]s iv)4rbPMuӕĽ^6Y@l| 8"-"\]`󅼑&jP(N7;nƆ݉-#>XKﮃ>e]c {}̝{b'hf% +@=xoZ{6w8|iWJ)HFi-b>US_[kJz<GφdWn̝#DIKTAMźpvn`FZCU5)n `e]da0׵47m5ۊ״2lmTN@Xsȼ#l 0Bmr`p%cFUY1"<5J-ܸ3i)Vn9jk  ]$URߐM -s2e}3:FTŎhTo@{Q]YIeԷVG\WgT#"!ţɎ yTRF0\H8t0w .$3>Lw;AR-qrqI M +GJ%r`+ʝј zsIzqM]RGTzKFRŸmAզ,ˣ $,_טJy=w L*Eլ1ˬGJľ{(\G +KP|E9YTS,X#ݕahiZ&j$# +`#M=dy;E9Rŷ/F5Pk>[JpdSʏ/i=oLյϻ{3mi[Szoܻ yLZ=_ըwJ|Md5Òԝ^ĕx+ߢZSFtj1c cRNLt skfw?Ashs)s~N;YAOv6쳍!lh"YR# endstream endobj -244 0 obj +250 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 243 0 R +/Contents 249 0 R /Resources 4 0 R -/Annots [ 245 0 R 246 0 R 247 0 R ] +/Annots [ 251 0 R 252 0 R 253 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -245 0 obj +251 0 obj << /Type /Annot /Subtype /Link @@ -3808,7 +3853,7 @@ endobj >> >> endobj -246 0 obj +252 0 obj << /Type /Annot /Subtype /Link @@ -3823,7 +3868,7 @@ endobj >> >> endobj -247 0 obj +253 0 obj << /Type /Annot /Subtype /Link @@ -3838,7 +3883,7 @@ endobj >> >> endobj -248 0 obj +254 0 obj << /Filter /FlateDecode /Length 1523 @@ -3855,19 +3900,19 @@ K ZW .= ˝= OU+ժƕ^i&>II iPvb3qoč^,,ʾTm 9Bc^[J𐼫*;T*ÉQQAEܱXUMrr"_|vOp+37Ā XY 'û tkw#vE."9v 2lƔPP40bo*lU𧶭 n8`?XQiɿchFxO@@v6s[=¹"w[-)ٰGgk-]o}TUq,QUrpJ=tz̏]yz[I`dqXqol<rrH\phiRu$[e|pG݇U]'\(Õݥ=ARK%ZS2%W(ZQ3_$aU^x3*F`U^GVW^9>Ȫ!ج(Fy6~16lPxp/P .JIZi+5o`h&A]no!v ΆבA8i ~<%\AL*@#fMb'L,? endstream endobj -249 0 obj +255 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 248 0 R +/Contents 254 0 R /Resources 4 0 R -/Annots [ 250 0 R ] +/Annots [ 256 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -250 0 obj +256 0 obj << /Type /Annot /Subtype /Link @@ -3882,31 +3927,30 @@ endobj >> >> endobj -251 0 obj +257 0 obj << /Filter /FlateDecode -/Length 600 +/Length 633 >> stream -xTM0 Wch8,\V@ 'urA4c;/qЁP -/Erbw1< s}Zē?}aĐ\ -qG@j$ oçPg!r1QR5]}=ìT%)-i)IF¶nuӣOoOxIpid*",YfW#6^y}2.8I% ;uRXI3||[R4_:Ww BPjh-Z Tf>`2TT_tbMwhc ؒRD Xyq@24_lf8f+F̛gSXʃ6^%'šKw;iS:dfƯŏ;MLnE׈ERy/ Sk5׳IM!{]z)hgm'}2 'e5 -RFLSrOF `d+Q1rIOz۶zJ*MâK:L;Q*{6N +xT1 WqR @BfZf./d<].:sbt+ԟї"9Ew1< 3>-O~4PY8$X +qG@$ oçPg!r1(p@WnD0k%FWISŔ+|kRlѽm[ 4dg1J c,{^j{ݼ>U$@`j(>CD~/;5h7SJ"J/HqhZ2ǣcT/6/KE]L L%JD#rI'mpWrYOҴ-wZ`g>U3|Sه?{^j endstream endobj -252 0 obj +258 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 251 0 R +/Contents 257 0 R /Resources 4 0 R -/Annots [ 253 0 R 254 0 R 255 0 R ] +/Annots [ 259 0 R 260 0 R 261 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -253 0 obj +259 0 obj << /Type /Annot /Subtype /Link @@ -3921,7 +3965,7 @@ endobj >> >> endobj -254 0 obj +260 0 obj << /Type /Annot /Subtype /Link @@ -3936,7 +3980,7 @@ endobj >> >> endobj -255 0 obj +261 0 obj << /Type /Annot /Subtype /Link @@ -3951,31 +3995,30 @@ endobj >> >> endobj -256 0 obj +262 0 obj << /Filter /FlateDecode -/Length 955 +/Length 1005 >> stream -xKo6 )H aK-PxC[de{2Nv2Y'kc)?>4)USՋi><(Lf'7;RbłkZSPR) Ĭ"T?}qRb9$RhO]>w02yW jMσE$4hN҇d6_ب>1M$]>r/q^5O"|[5H O 5O?np!@FxP̼LpRomA^k{(慩ETJr^7\1qQnj)Z9-6a,2͇UF݌m4qLH7Vt4Yy?6bK-+_Ţ'l׼_2*>UY*S3GTn1e}]E2H`{Frf %)cHB<8p= 0:pNbS&+()f(ĺ/7O|Xg{R_­\)u'"ZpcosI샍6:b>7dnazJ= CT"/r'WXqi<WX=kceх1|-5pg2c{nΒ \QZOɸ/E9TC3޺;_px;E\ݰc:ffۮjt*գ -ڨ[R}D!`t撵GOݤ<0 ,WR@ ",**/Px =;Ű狋x;B%J)S@qRT(ͤsni :n=b1#O-{URЕ- -p[Ug0m0Z% -]l1;ƙgp"8Dl'Yji5Nyw[mmͼoA) +xWM6W9CH{H{YE = +rhM?GJ[;i,Hp4p;!Gr=%!E}iK&b>[|v I Ru׮O\"J9u3;YMu͘hEs&&xQ&^X_]Ѹ|gGz9d<1wo`՗!w|X e` AY~{yr!h a;k\prvEf^\TkQ$|^ 2ag쾹Wvqy E 1 Ʃd0Og|GN%(U vf\P=yQq}Ӭy9ˍt$$C\/;s CeLKR-1\:#=7]VO*ՄXuXV=|Ihq2P?ۙ+(<2N%;E'x-q#W*aqPהQY m;U !+ Wƴʠ Aso0: @1 +'o{ I1…*7ҁ_COW9ǒn~sW כLI,`cH:;aȣ 2JY~^xGP$s[ `f땾g6Tz6/+ U<ݩ CocvF8]iO_?R>&2sH[CmH$ϑcbN "qBlU=Г$=O 2h(7ߡFTC6X ⣄|oڂf9;Q3hHK*X!Vlj3]UAj %.<%yqj`(?c_R endstream endobj -257 0 obj +263 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 256 0 R +/Contents 262 0 R /Resources 4 0 R -/Annots [ 258 0 R 259 0 R 260 0 R 261 0 R 262 0 R 263 0 R 264 0 R 265 0 R 266 0 R 267 0 R 268 0 R 269 0 R 270 0 R 271 0 R 272 0 R 273 0 R 274 0 R ] +/Annots [ 264 0 R 265 0 R 266 0 R 267 0 R 268 0 R 269 0 R 270 0 R 271 0 R 272 0 R 273 0 R 274 0 R 275 0 R 276 0 R 277 0 R 278 0 R 279 0 R 280 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -258 0 obj +264 0 obj << /Type /Annot /Subtype /Link @@ -3990,7 +4033,7 @@ endobj >> >> endobj -259 0 obj +265 0 obj << /Type /Annot /Subtype /Link @@ -4005,7 +4048,7 @@ endobj >> >> endobj -260 0 obj +266 0 obj << /Type /Annot /Subtype /Link @@ -4020,7 +4063,7 @@ endobj >> >> endobj -261 0 obj +267 0 obj << /Type /Annot /Subtype /Link @@ -4035,7 +4078,7 @@ endobj >> >> endobj -262 0 obj +268 0 obj << /Type /Annot /Subtype /Link @@ -4050,7 +4093,7 @@ endobj >> >> endobj -263 0 obj +269 0 obj << /Type /Annot /Subtype /Link @@ -4065,7 +4108,7 @@ endobj >> >> endobj -264 0 obj +270 0 obj << /Type /Annot /Subtype /Link @@ -4080,7 +4123,7 @@ endobj >> >> endobj -265 0 obj +271 0 obj << /Type /Annot /Subtype /Link @@ -4095,7 +4138,7 @@ endobj >> >> endobj -266 0 obj +272 0 obj << /Type /Annot /Subtype /Link @@ -4110,7 +4153,7 @@ endobj >> >> endobj -267 0 obj +273 0 obj << /Type /Annot /Subtype /Link @@ -4125,7 +4168,7 @@ endobj >> >> endobj -268 0 obj +274 0 obj << /Type /Annot /Subtype /Link @@ -4140,7 +4183,7 @@ endobj >> >> endobj -269 0 obj +275 0 obj << /Type /Annot /Subtype /Link @@ -4155,7 +4198,7 @@ endobj >> >> endobj -270 0 obj +276 0 obj << /Type /Annot /Subtype /Link @@ -4170,7 +4213,7 @@ endobj >> >> endobj -271 0 obj +277 0 obj << /Type /Annot /Subtype /Link @@ -4185,7 +4228,7 @@ endobj >> >> endobj -272 0 obj +278 0 obj << /Type /Annot /Subtype /Link @@ -4200,7 +4243,7 @@ endobj >> >> endobj -273 0 obj +279 0 obj << /Type /Annot /Subtype /Link @@ -4215,7 +4258,7 @@ endobj >> >> endobj -274 0 obj +280 0 obj << /Type /Annot /Subtype /Link @@ -4230,7 +4273,7 @@ endobj >> >> endobj -275 0 obj +281 0 obj << /Filter /FlateDecode /Length 2565 @@ -4246,19 +4289,19 @@ Q Qnr`g4k5M9bEZJA+"F]bfoN,GBօvyX%5dd:%lJG d>?!L!Ab_.v>jmZYۚbwH1Fi7OL~40C囑3F>${V;сQ" NX''e XO`?CAc٫!GG;H(>pFGOϦ'4C`9BUa:)@9elBx+EBMJy&??vl|`If,HgCzOY!}M)K!$Yjy#Eȕ~8'2ʝ!/0x|"@`zA˨ u|' 弇Dyn$q&]6$BбcqZcG$ XSZsbtBη]:QPlINl]5m!,%]i|Ldy:QOob3b2hQAnA;?i![jV^]ϓF8Uň붆InuѺ$a/-DfLZ&cpE9}6`_VZ,̩Jq^~~UܗJ|&9tn&5)~-t]OVȻ/xR'b[fL6 i͜ZS8vRi.F@]0N1可b$\UaAƫp.xTprKi-g@uVr[iw?Ѵ endstream endobj -276 0 obj +282 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 275 0 R +/Contents 281 0 R /Resources 4 0 R -/Annots [ 277 0 R 278 0 R 279 0 R ] +/Annots [ 283 0 R 284 0 R 285 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -277 0 obj +283 0 obj << /Type /Annot /Subtype /Link @@ -4273,7 +4316,7 @@ endobj >> >> endobj -278 0 obj +284 0 obj << /Type /Annot /Subtype /Link @@ -4288,7 +4331,7 @@ endobj >> >> endobj -279 0 obj +285 0 obj << /Type /Annot /Subtype /Link @@ -4303,31 +4346,29 @@ endobj >> >> endobj -280 0 obj +286 0 obj << /Filter /FlateDecode -/Length 884 +/Length 922 >> stream -xVMo0Wz<-p(\*- q&ġ/1H-E$ngo -'8!DN w}g4+[l^[iHN D4(ap>w?4#j6+k5At?|zF}MC1Z\ћCDHG^{:uW>||Fш>0u__˃JsN>{ȟ dE|b8+ʽ N>N{۟oS,m덼Hd6l yh}+c޹{..f٬R8'e>$9k]֕~~:CFS%gK)ea3h,dT\1sOvwx\ "nC`ݜ@ T5z MB wAoWqh|˲*1ѿOq9j+asuOF; Vzݓ3dd۽>յ=2#z'|W%W^ϕ} j?VIEyg+u%YXK~=IssQG[!4T4!ߵLzV6$Y^ҿ?ϗn~YnV{+m֋7~/oX^2"<ѷZ,$Cl0)E,Kiehls\%(DI±.k};٥9?WcS{S> endobj -282 0 obj +288 0 obj << /Type /Annot /Subtype /Link @@ -4342,7 +4383,7 @@ endobj >> >> endobj -283 0 obj +289 0 obj << /Type /Annot /Subtype /Link @@ -4357,7 +4398,7 @@ endobj >> >> endobj -284 0 obj +290 0 obj << /Type /Annot /Subtype /Link @@ -4372,7 +4413,7 @@ endobj >> >> endobj -285 0 obj +291 0 obj << /Type /Annot /Subtype /Link @@ -4387,7 +4428,7 @@ endobj >> >> endobj -286 0 obj +292 0 obj << /Type /Annot /Subtype /Link @@ -4402,7 +4443,7 @@ endobj >> >> endobj -287 0 obj +293 0 obj << /Type /Annot /Subtype /Link @@ -4417,7 +4458,7 @@ endobj >> >> endobj -288 0 obj +294 0 obj << /Type /Annot /Subtype /Link @@ -4432,32 +4473,29 @@ endobj >> >> endobj -289 0 obj +295 0 obj << /Filter /FlateDecode -/Length 920 +/Length 948 >> stream -xW͎0 )dĎi.+  qh;SK8m)+4g^ #(;/cOH|,+dz> ^hceZEh{}bs`"S>?u7(b(Z *Ψ> $&Q6[o>yxQO.X?M/co QDB8"PY Ny6`Z$޾~}Z6]i>}O -AzjU$ia#>^NdQvE"䴢L"W] p6+)+FkusE.ډ@|T@zӆlA;l7OM2~f<F3g392@Y&GP,Dgv`UnJIUPYi%* >f%5\o7=TRm}u]m&,\ұ q=]oo"rjM_4*m#0S!yuVwDV2yX&pŹErT ^ ( '&+qܫ ڒB cދ=M"Z|sƱ}puryK{4o -kJ;@D{ٓ$Kԧ2Y9 -y,ؗXH22-,@ɀl7o, qƔkIO)YK*k -08-%cɖ 0G>ԒO/6|veAX1i.&-jx:sY^:QL;R9r-^Y:Kdʲlx" njq 0]BC)`:{s),Gw_xwbqbL1;5b]1]讆 +xW͎6 )UD%`!e)ZbBٲnҌFOG/p\|GZ~y8a7T=02xIkǏ!`k͞GcApax3O* uF.f L4ye_x]Cgԋ /|C,ӿ QDBDx)fŝ68Iۀ#*k NjX/o7I=zY?d`/CBBc"IK4vr˓ c<ܚun~2d#E!sZQ:W1CVKVR E Ӓۜ+r*SAf?_@"װ}ni4_oa6p3]lW{5ÃU&@zz}6fV0BO>I,Qbxfe?3~iFJ ÿq2U%lֻ p;d+5OPP26.tnGFKs`R\ `[x̒&%(iQt nawRǕm5 g;UbfwO>]_*A۠ybĹ5q?V[6sx}mjbL΂_&SY2f(:4 j!Kԗм* ⮌$#^>vR/%(}xS9fgLIw^[G,zYMW_`FEKIIh-~:: >պ:bmژr=>4A(ei*F +u'vA` SySY9tȾe (~FGiљq0]\C)`85=@IDpRI WPn~|~1F?q~1&#δ@w}  endstream endobj -290 0 obj +296 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 289 0 R +/Contents 295 0 R /Resources 4 0 R -/Annots [ 291 0 R 292 0 R 293 0 R 294 0 R 295 0 R ] +/Annots [ 297 0 R 298 0 R 299 0 R 300 0 R 301 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -291 0 obj +297 0 obj << /Type /Annot /Subtype /Link @@ -4472,7 +4510,7 @@ endobj >> >> endobj -292 0 obj +298 0 obj << /Type /Annot /Subtype /Link @@ -4487,7 +4525,7 @@ endobj >> >> endobj -293 0 obj +299 0 obj << /Type /Annot /Subtype /Link @@ -4502,7 +4540,7 @@ endobj >> >> endobj -294 0 obj +300 0 obj << /Type /Annot /Subtype /Link @@ -4517,7 +4555,7 @@ endobj >> >> endobj -295 0 obj +301 0 obj << /Type /Annot /Subtype /Link @@ -4532,36 +4570,31 @@ endobj >> >> endobj -296 0 obj +302 0 obj << /Filter /FlateDecode -/Length 2274 +/Length 2290 >> stream -xZ[$~_Q/jst@ТAyx%T8T3\r?9蝙.>쭀<{קVy7E3daR( 7Z4G={!|JVFl!ӻEb[*C#`4'XXfsT9=~z)ʅ,~z{Ȳ"zզc,#^'! bJU2'mzu9[~|L .UvY L|\K1& EJ P(w~*+A땔B!h-Ʋi +xZێ }ģu $H&@0A "l%2E]FuWٺ"yxHNc +M_N_Oj>_dfi +:Z+c38kU~~8zCsP*0im :yo?>-zV=qu+]e(c.IHINӧ?߿}1&??+e㋋_`V(uփ5B[.\ģ;ڿOz-BrgM:"?~j8\{|y ߴx ŗݮ>)ڗuʟuv_V\yY.}[ׁ:~x{\Ú $r.vۻ Bְe<_eйd9O#˂Cpal1b6ڲ?LwŪ?7Gh BU nJtČf$o89@sK,S-޴&yir\kURE[U9d,p5+{ 'zʗ:>1Yx5խ5n< |20:mkKҟ-HX>oȭߋ/rkC'о'cW>MÄ4Zo_'<~m9_jv&s[LOɦ]!Ae7ex#)+,m7fl(tGk|p_yC~G7eCV h`I-G̡냮ƴ@̏Oퟄj9D;$ҌcARGNY2pbFR:Mq,%5Gm:AcV; YiоM#0"$OX5e꜒a!UU|o? 3A@>5:.杙RfvCf'qCy9vo6Zdk}06NGc!M1X-CUGІ ;Th9"gĢ9bfˆntjF>b ,\ԡq YF,s$"blL?uRg#ljaVIBh iVEYZQ(Gc9ʪe=]и;&<\m@:@N/UZ@(Jyn;r8Nab8f&E, ?h`h</g-tFȋF@JZlL֠BDDMwwf2?oBv::V7y7C^Ir8Ǣ;Ct6y(6r"O!>T,I_;JEIڍNJvr{l^KİQh."dD: fqkq#%ѷco@(v r*EE.GzowPEyVYTFjqnYn׉a%_:r7h":zwF/7,ꎳ!58]o ͟Y{LhY'ޟב 8?.PgixX8v>|+8Q(،q V7?pQ[\yV%Q€] i6#rÕy@'[6?M{ÿ8DZ0STX4X3cl8&DCHX%f}nRـѭ\^sy# S,kS]ւSB8a),Um:" / Eț -GGkYpg endstream endobj -297 0 obj +303 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 296 0 R +/Contents 302 0 R /Resources 4 0 R -/Annots [ 298 0 R 299 0 R 300 0 R 301 0 R 302 0 R 303 0 R 304 0 R 305 0 R 306 0 R 307 0 R 308 0 R 309 0 R 310 0 R ] +/Annots [ 304 0 R 305 0 R 306 0 R 307 0 R 308 0 R 309 0 R 310 0 R 311 0 R 312 0 R 313 0 R 314 0 R 315 0 R 316 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -298 0 obj +304 0 obj << /Type /Annot /Subtype /Link @@ -4576,7 +4609,7 @@ endobj >> >> endobj -299 0 obj +305 0 obj << /Type /Annot /Subtype /Link @@ -4591,7 +4624,7 @@ endobj >> >> endobj -300 0 obj +306 0 obj << /Type /Annot /Subtype /Link @@ -4606,7 +4639,7 @@ endobj >> >> endobj -301 0 obj +307 0 obj << /Type /Annot /Subtype /Link @@ -4621,7 +4654,7 @@ endobj >> >> endobj -302 0 obj +308 0 obj << /Type /Annot /Subtype /Link @@ -4636,7 +4669,7 @@ endobj >> >> endobj -303 0 obj +309 0 obj << /Type /Annot /Subtype /Link @@ -4651,7 +4684,7 @@ endobj >> >> endobj -304 0 obj +310 0 obj << /Type /Annot /Subtype /Link @@ -4666,7 +4699,7 @@ endobj >> >> endobj -305 0 obj +311 0 obj << /Type /Annot /Subtype /Link @@ -4681,7 +4714,7 @@ endobj >> >> endobj -306 0 obj +312 0 obj << /Type /Annot /Subtype /Link @@ -4696,7 +4729,7 @@ endobj >> >> endobj -307 0 obj +313 0 obj << /Type /Annot /Subtype /Link @@ -4711,7 +4744,7 @@ endobj >> >> endobj -308 0 obj +314 0 obj << /Type /Annot /Subtype /Link @@ -4726,7 +4759,7 @@ endobj >> >> endobj -309 0 obj +315 0 obj << /Type /Annot /Subtype /Link @@ -4741,7 +4774,7 @@ endobj >> >> endobj -310 0 obj +316 0 obj << /Type /Annot /Subtype /Link @@ -4756,41 +4789,35 @@ endobj >> >> endobj -311 0 obj +317 0 obj << /Filter /FlateDecode -/Length 2630 +/Length 2643 >> stream -x[[;O1_ nGX )M)ilIK=^>%JH#Kv=w.W"qZ05zY gxI#Ӌz,^3p~1!TR R.?YέR28#`|/жT՞y k,V*")D}8Ϥկ-jn]닕,^=+?D&g@oA+aS -[X/"xh8n/ owd -cԜkHLrg) -WAc giLjɴpN,] /' qL1h"s~Wx^?&䚆L3d<^ʐ'/^3!DZH&$۽\!ູ}d4Md:Q9YJUHE8|F:݀SV辍V\v"͛J-Iu˟FKĨsqP(+{fB-Į98<$zA$T)JȀJ7ܞˠԇWDcyGܹkMa)ĴVwD ި}bkQi>*u4^g5JJn{lA ƚ/_Avvd43 5r5t^OM%"!qH1Y(T1`aMBG%,(G<bc`WԱn{9E@C~}<&$hLTB爣kQBUhxڑ⢗9ۉ&jhkUt@ mRj@uh:eOK#K:E;),e%3@,@5o ] -Qi(TҺyKmE&βBzCa"e9IK˚BD t`І>~{OmJ>V~ 4czqC`TX9NzsHtb'6wIQASP{!RZh5 l"i@C#v '.˟;7sDQŰI*֭ƧRe?5 iKR'_ɢB>NTUP3"AP\ h ^+C;':T^<0X?)]my وcw,4p^y u9 X95!cH'F> ]8f;l9Gb@ -Ck@Q$( -$!*w$q z }xIU]`* F&8LmdIbB(({u`lܜ tJ_WnҴ:Oϔ~\0^3n,br3_ - -e#۷C!Dq6~}1iҞ ^:s9?'1{ȟ`m)h40% p A!|C7r{94וƾpnZ؅$TDI.5Jdwao&D:ݦ!(k3A~x` '=nMG%(G"jOT$;_Hf2O -IHzI8ˡ̕A&OןiWQ]څ̑iftf5:bqTyXȮ5Li#B\۷L ICuh:'x|r;վk|qy/سhv.%\vY3&l/izY7;=z٫ޤꟾz{?:~w&/ބwom Zi 􃾞zEJ&{!ZA'/*qChNى[$9 ,d&6=Kg"Y,<}%N 朷F/^~z҈K}x,5L;'_pL.U/?%>obs" 7Ŀ|,_~-,-UgEHte42Q'3pzZ~Wbz9 WcJ-O"g{6Y"h9EXǔ!V~.(η_uBi& \$&itP'kgKx3W]¸9c1 K;j giYjɴp#' qL&fmn 2ss۽~L\5 3gx&!O^fB +!2/yS%zK219d\7 L!Q)dmA,V!htLUX6VZr٭6o+Y&--Y J{ύsC5 #c DfP!*!*A4r{X.Rj\ebZ|ĝ{f4OLlu,L0ꍚ'}CZR\gHh{VT-OV$`kiGF<kHQ#WCUZ"kBv`/tY"?pb|}Ë9,6v%K붗S4zj>7чs``BRID%x8zVś!(tQH).zYhVE0|`,šDi\3Y44QJܽ RV24$EPOP(iL[:iJwBH J]\8_pG ] pBhh]5i`:3DF39 OhW[zm,t6ĘK =WHow|ki8@Nc}iIiQaFx:<dфef;+Ъ1hT4I>Fx?IJ)I\;Bh~-H>zhU6Hw}pka!:'F1qE*Yb47g*&bh,݃[43=E LBو-*w@!Fxr\Mo߼yqLꞂ'zƭWa%oFN\w E'15!+ځDYۦ\!{Q4*9wA50dB$L;HܑnJxXB2@% +I#)066QAK3o6lhR15 W "h)= }% nb\*YN];>Y[Lz+ +aΜ&MmLeզd/w/M1K>FYijP̹"&O '+HD~ =)!꼭vl)s6[V +W/Lӵt1ojA?") ͙1?H /73KzgNZssbd +2b̳wW<y&51#m7hA'l66v~"QKu۷vl.@cnZ RGb;?SI*B<f9$=fT4'69tQ $c>)&!i5'(/F0Wrܓ(fVhOv~}⬍\ .A_g\J׭ţ?gqpg^legfWovz>}f;WoO_I?}&~fuM@_. s>4s‹8v7zIƓ' R h\ ƽ1"oI8md'B'Y`!3P_8ぅBEx(WdCCSHH +P}Ff  endstream endobj -312 0 obj +318 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 311 0 R +/Contents 317 0 R /Resources 4 0 R -/Annots [ 313 0 R 314 0 R 315 0 R 316 0 R 317 0 R ] +/Annots [ 319 0 R 320 0 R 321 0 R 322 0 R 323 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -313 0 obj +319 0 obj << /Type /Annot /Subtype /Link @@ -4805,7 +4832,7 @@ endobj >> >> endobj -314 0 obj +320 0 obj << /Type /Annot /Subtype /Link @@ -4820,7 +4847,7 @@ endobj >> >> endobj -315 0 obj +321 0 obj << /Type /Annot /Subtype /Link @@ -4835,7 +4862,7 @@ endobj >> >> endobj -316 0 obj +322 0 obj << /Type /Annot /Subtype /Link @@ -4850,7 +4877,7 @@ endobj >> >> endobj -317 0 obj +323 0 obj << /Type /Annot /Subtype /Link @@ -4865,37 +4892,31 @@ endobj >> >> endobj -318 0 obj +324 0 obj << /Filter /FlateDecode -/Length 2042 +/Length 2088 >> stream -xZ͎7 )r(^lE3v&!-?PJ#JFw޵"?~ w~y-Y\?_\8sOZѼI痋r#]f{gBpRh[~vwA1ǹnBJfDb.oVU8lHk8(YFH-Nϗ7O_gJٸ[x;;ZKi>}Mqw.O˙96.qwh" RZR)|t=3*_Q&[}$ظʼ6&ڙg{ר|ݏkɺ֮۱&~I#[׉P5o+ 3-9Hh@2j -Ө30!Jjt Hc-{e :ԋD"n_~?q2XzI `8o0L -lSaϞwlwl98ePq -'E'5}F#R0,EC #`K$n#GDR;&06 TiDaՋrun8&ͺmRRO?X WA/ -&^g~Ņ AC7F&Uʜx*Wc*7i:>)W9# t5)ڦtф,ڤȺfY3| }3Wھg!} h -a1D#Lm,,"o)'I\r -\Z7F\7^ 5tuOc ¨9ad..m+|`G!7_3i~VGf<9ftֹI;i bx.vF:, :;tFcYyIK<32:osx(IvBiIFE  `nGZJg- jF`7Bd fgfAґ» Vk;F*Mo6FhԾ߲u7Gig=v@Yr6w -8:ђlPWwʄLhSH )QK"H&]r i9 Ms*й)Nbfd`04Efҿ{ #Ϗh,х}l03>H1u; 5i|B-iB5[|Nlj/GY#t 1Uku`$XZC)iC+jvw?By-bV90^mNo;B3'7y=ͨ{T{05%ǿ)k81 ../E]~ `^@±( ̋~)hnEqS78\c|~`rmUellCr"wX6c>&ۄ.vɲŹ3;uC*a<[==NDkssP1.#E)7$kI%AĠ,&?*uĮ,'sagO?\PwU֣weUM7ggY%:,Ɓ2YEw~bbI2Ѹ}A^TfUV(i!.z6S7a;)FI;E:4am) 'xx\2˜XzףZڂ>a$!+X(GB諤p(8yfIQEƀLR(r¨80$,P_A>ڐaA-xH3\9&XUz,tNW@Z(CWD#ME;sHkۦѳ:y*n /:=]# rF'mt`n=$ny8PxQC1|P'sn̝(Xe`b:P:~A#`H\ifX FhFuTߋ!o~| %r$AA:v@Yٞsp_d%7Srogwq`i2wqQ"JPXI5i98<2* ɸ,)Rdg0U04f^e3>(h:2h0IGsggˬhFܳ<؃q(M_neŖ>}]jsgz1PKjM.Tu"38! MӦ? 3Ty>fݢItj{X@¢ݼ Ć6P@'{ ;ẁ[fvfUoA@;=z J.>S[vǵEZd,Vqg$E4;z$PLhI3!>2d^YDBQQά'Ҥ:rE4;B%Z/,c4":ެ] "'eU3Rԃ_#a:[8]R־Ӣʴ$G3|{IΚ(.A]#8fX\=X/ۣVzs4mrp]}$50:^o=ƨ=-Y+\f_o,#M+^j!FS_'LNr:ZK JaE r6H47+RXwEH6}T2D j5tt8ձK? +&KJWaףy #s +pXj $~./YKnʂD'~?oD endstream endobj -319 0 obj +325 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 318 0 R +/Contents 324 0 R /Resources 4 0 R -/Annots [ 320 0 R 321 0 R 322 0 R 323 0 R 324 0 R 325 0 R 326 0 R 327 0 R 328 0 R 329 0 R ] +/Annots [ 326 0 R 327 0 R 328 0 R 329 0 R 330 0 R 331 0 R 332 0 R 333 0 R 334 0 R 335 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -320 0 obj +326 0 obj << /Type /Annot /Subtype /Link @@ -4910,7 +4931,7 @@ endobj >> >> endobj -321 0 obj +327 0 obj << /Type /Annot /Subtype /Link @@ -4925,7 +4946,7 @@ endobj >> >> endobj -322 0 obj +328 0 obj << /Type /Annot /Subtype /Link @@ -4940,7 +4961,7 @@ endobj >> >> endobj -323 0 obj +329 0 obj << /Type /Annot /Subtype /Link @@ -4955,7 +4976,7 @@ endobj >> >> endobj -324 0 obj +330 0 obj << /Type /Annot /Subtype /Link @@ -4970,7 +4991,7 @@ endobj >> >> endobj -325 0 obj +331 0 obj << /Type /Annot /Subtype /Link @@ -4985,7 +5006,7 @@ endobj >> >> endobj -326 0 obj +332 0 obj << /Type /Annot /Subtype /Link @@ -5000,7 +5021,7 @@ endobj >> >> endobj -327 0 obj +333 0 obj << /Type /Annot /Subtype /Link @@ -5015,7 +5036,7 @@ endobj >> >> endobj -328 0 obj +334 0 obj << /Type /Annot /Subtype /Link @@ -5030,7 +5051,7 @@ endobj >> >> endobj -329 0 obj +335 0 obj << /Type /Annot /Subtype /Link @@ -5045,37 +5066,36 @@ endobj >> >> endobj -330 0 obj +336 0 obj << /Filter /FlateDecode -/Length 2169 +/Length 2183 >> stream -xZ[,5~_r, GVTćٝ*G?XI'Օtz.awtRz`O0 BޙA -o%?|}:h/|ʻ)aB'AHqQkaO~9}H?(%,DlS BN;DmwjOF;3i{ܧw|~Q2H<|dݬ?E M)P -˧r(RG': |Z[3= ?7~7Et>wsB)Bɬ@B"FS(T9/h={uioGIa)W€7sbp+DL<}Ie&q?t WxW̑ݛhz0gW/gem˞Bg>.\3V>*;ѯrNo#}=Ug(穰맳*tBN5]ؐB(Nbcjl,τflW ~Z!* )8Zf>{w‰W[\(gqA358YTXy -̝q`)$ս640y<֠8ez߹&bz:eP]"2dsd BFig*\q$5g-/Wk9VZ?`ʹ7"@ Xӷ)x3I}gJH X8AD,BnDmܱƹ[Tbf\v` 3;wgF[MX w?ݹ)k!-. E_?5 m,"GTJVg#v:Jw;דϞ X|AFlk -(t)d7*nMZA/P6A18kM+2(cOqb[\}06WW RdW|a]cȒ =4Bj(+dC?vru_/F#*aZ-k *Ψ - - -4a*bnVRB2p<1ppVCޞ0E,h5rH.[&/R }vL \@Zp*0 ̠aiV@Nc ʸ^ !SσWB a街 V~X霴"l$(WlܽG(#5N &yϒ Gy+cݦH 7@T]Gwg:P`-/ty$jN#fH)VVfiAZԾAdu_E0lEyu#жjA@jmZYhգ_1q+a|;]3kp*u6^*oB}QYsw{PCBņx}2,6طc:TfT8JWqC:g aGWy1ޠQia?I[k.Pڬ.ztvGAw}<z3 |ӛ[ޢ7?%o~z+`szf_t7W%G̏V --F;??8-^fPz^vzyWb4P~Dt8թťOɒ\aRy̹Iǐu:a.Taы&0iō_ͦɊ D>aꭌ܇Qr~kJϳ|Y~]X'!J|~6E]Jo\z5uLJ 'wph:ox"*$ ٲM!x\ȵĿ@J5*Z>j5$:6k5 L;sD"cz ׻0P9 3 +uYglI@yR6(K=%+*V*T7~7z+ɺAV+2^sF[r5:8;(ˎ,XB[ x8h۝(U*$Qt>v6/D$w`z.;871̬vLAS|&لYKQD;&?⒐S2ހ `Qi^W  Rg5̣-]KL +[78QiżmE9^/5ևaaMh'<@cMlD 3Afah9b6+9@ogTȦcpAYxmJЁ# Ɇ1[Zh *xM"pZuP(>6{nH]oGV`WX+M߄iTm00%4 +Dž0ׁN4_E'ˈJ*7E5xܤ6r.}iMae+ C<^2("*2G8b@NyKon+\tk0H%y̳p:=GQ [uW|g^%E4+$9g5 hy/̏an2LIJlܵ}mRG~ |o@޹X 10UNDE9;Ϝz%U+ۙ%Ӣ:%_QP{=>,| v-z}PaU;ղ& 8_xqrLaTQ3nJ-юZ<ό؊yϬΈUky,KU@ +dt]3g/ą -6Ôh'¾ KLEFzw[sVMw;CJPہ:JUMzPZs)m"@Wf pIQ_l on>q}nҕW.1(T~ݫY^ z,y+\ֻ70}8_ݫ1?VXΔt{&{Bo [oIU\I¬[ǼqʈF-&O $&E:`xMһ- s.3eΏ^50R ,s +`? endstream endobj -331 0 obj +337 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 330 0 R +/Contents 336 0 R /Resources 4 0 R -/Annots [ 332 0 R 333 0 R 334 0 R 335 0 R 336 0 R ] +/Annots [ 338 0 R 339 0 R 340 0 R 341 0 R 342 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -332 0 obj +338 0 obj << /Type /Annot /Subtype /Link @@ -5090,7 +5110,7 @@ endobj >> >> endobj -333 0 obj +339 0 obj << /Type /Annot /Subtype /Link @@ -5105,7 +5125,7 @@ endobj >> >> endobj -334 0 obj +340 0 obj << /Type /Annot /Subtype /Link @@ -5120,7 +5140,7 @@ endobj >> >> endobj -335 0 obj +341 0 obj << /Type /Annot /Subtype /Link @@ -5135,7 +5155,7 @@ endobj >> >> endobj -336 0 obj +342 0 obj << /Type /Annot /Subtype /Link @@ -5150,33 +5170,36 @@ endobj >> >> endobj -337 0 obj +343 0 obj << /Filter /FlateDecode -/Length 1460 +/Length 1473 >> stream -xXKF Wd2‡!%-P VCZ$@9/-;6]+y|~;с0n|r -濼b#OE r1B!*m,ZVc|!G -ƈ1 ->wQa>EF+&~x=@S$hDoxCRu:aQ\[ӍL 7Ґck:z4/M64N׽uqxSu}kTt;6kpW9X_ŊhŊB#+fm+ ̼S>LX># *㌲&&5?`Xh櫸EIJ['Lj ZPXQHQe`ba#?\ -l]ݙS؞4$ȋ 9鐂ńS ښs~7;RPkKshF2)5nlm;t-)gb!Wl[i|Ce[*"є҆LWٓIK 3Vx;4q$ -y*?PǧJ_ӭ φ̣%fR֍/GA\#O +&d(kOnY-k2;4֓(-8=vf <aO 5Q~fo{"uӸ8ֶKr-2ƪט],[Q")ov}#UqiX޾ bo3U`r3`6604ލ*5&xv -P]iSmҝ?d rW mj`zI]iJV0FXc$эwRpjV߳iik%Q..VH_fei)W=Y=NH - #hzmqX:,q(Jkx8&iޛ5kֶsJeM֎d1~jcYگ;MQKIb5`G_kՙJ -ΏC9\'o0pe'jpw&f)!c}ȈW!Ђ\ p݄̎p#R"m#?nw#Ђ\vc =S6?9w30R q7r78pn$@7Qlr)׌]QhviO$XtWMo}uWTo}}+v|"W5WL@ХlwVaE֗%>jTm<<%E$9Ǩ(D΂%(&A!\ p|:&TZ7,͍heS{Zуs Y +xYK6Wk8bECK-ZAH_HHֆVDOC.tD*FA+?yG~ + 9bHBTXiZݗ?4*#v(O#K/O=(}0} yV,zH@epo. ɕֆMqQkoo`!cB6beLes9"3O>uL{۱>0D+ʞĚ+PTxbR!5F+m-`jEbQ+W#EUE +rs1/Awqf1=!dh)? 9ꐊńc ښs~7S TڒZL+|&֐rٷ䜉ED2R _[֮3=Wxb#d> Ő׊L#)/`he Aev&I7mzjA} +6POUG^M^U>"f֍UoGA{X/ $`_@KL1E>UAע̵kɰ/ ѓ(#89ڶS-󮽔+ +pS+HV:jhi?gue3ַn K:i\lMإXz\tVckLɮ/Ȕ8M>P{(sIZ)e7/Ap7- &wt 8^.\j,9 I?ưozF \*Hu' 0Xx 1 +ǚ"nIKYmZD"\\I_nZWELh%-qG*H,> endobj -339 0 obj +345 0 obj << /Type /Annot /Subtype /Link @@ -5191,7 +5214,7 @@ endobj >> >> endobj -340 0 obj +346 0 obj << /Type /Annot /Subtype /Link @@ -5206,7 +5229,7 @@ endobj >> >> endobj -341 0 obj +347 0 obj << /Type /Annot /Subtype /Link @@ -5221,7 +5244,7 @@ endobj >> >> endobj -342 0 obj +348 0 obj << /Type /Annot /Subtype /Link @@ -5236,7 +5259,7 @@ endobj >> >> endobj -343 0 obj +349 0 obj << /Type /Annot /Subtype /Link @@ -5251,7 +5274,7 @@ endobj >> >> endobj -344 0 obj +350 0 obj << /Type /Annot /Subtype /Link @@ -5266,7 +5289,7 @@ endobj >> >> endobj -345 0 obj +351 0 obj << /Type /Annot /Subtype /Link @@ -5281,7 +5304,7 @@ endobj >> >> endobj -346 0 obj +352 0 obj << /Type /Annot /Subtype /Link @@ -5296,7 +5319,7 @@ endobj >> >> endobj -347 0 obj +353 0 obj << /Type /Annot /Subtype /Link @@ -5311,7 +5334,7 @@ endobj >> >> endobj -348 0 obj +354 0 obj << /Type /Annot /Subtype /Link @@ -5326,7 +5349,7 @@ endobj >> >> endobj -349 0 obj +355 0 obj << /Type /Annot /Subtype /Link @@ -5341,7 +5364,7 @@ endobj >> >> endobj -350 0 obj +356 0 obj << /Type /Annot /Subtype /Link @@ -5356,39 +5379,38 @@ endobj >> >> endobj -351 0 obj +357 0 obj << /Filter /FlateDecode -/Length 2454 +/Length 2463 >> stream -xZ͎6)}v%@  ˭ "XT"e; 2U(y+F -:s~_ZQ}I?__4 - V#҇+J/E5Z^peR*% uičCZ"~HāGJi]>}o7jVQޗOtx 2[`5>.s|흮x1gy1DJ -$FeU֐(gyޘN$K?j2ܝ?Pݭ62ߣ: M|>6*\:M; g.20VDZU&mYju5,qP -4D՝Lg2Mf=6uto2{ o]=]}YPr~nI ўa^[l@.?HI'/4ٽol ִWz˺4=M=gy~ok"';1"Rqs)Wc3ż3)ti6 I{u'AaV/-aHƃZ,[ow{<)AbؔP)1oQgfV\M(TDa! ܕ ѐBNq5DI1 W%M=3*.D 3mpQI|) >s>VJe|xҋ;L+ll 62\ \u|FZ D#6|Qfs$0~ȡk7x wO\ -/UUio::񮤹V`d|x_'lA g.^ gze(S!o\Ʊږrx!NvQ -8ȡy~dZTn6J?SOGռ'@+ ¤LޗNoQ 'yz -P{"׼J"=zL^eFн:>CoF͈w Bg1-*9XRGVseIw3q5mX)k;nЃĈPNJ?ÛA~1:9VE߿<;ygIlOc 0&^8|"W -(ZXģo4zuHN kOq'G/"1U[09>]~hI;[l_^iB9OL[nx&\'S`el +xZˮ6+aEff&  B,2dbQE$pۖb=N=)y+F +:s~_~.~hBw FW^H JNjZ kq˺R*% y]$\em[ѿtH"E 8VJs}WE\~ [*3(?Ƿi隉>o.| vVRh'1*í wF)']lC K4Mvk+=ec&ӞwC ?)fC-4-71"Rq;8 VcP癔Y I:ǴK]FS Aaz)#vklixR"wQL)1=SboQgfVAD*w"7kxhH.w%r4%5k@M9QR UVq$P܅衰 şQl#S*u!YbU㠱}{bXUßa0}tfXe`զ(qU %ӱgXٟi4mz =|F5N?DWkQT2'd9{| = +'zؓlվMr gXhLjgUx[0ukM@hSpC'OЭ85!~n|]߳r˾ߙsrl| abe*E&P!+onAP +6o ۬ <Gٻ-yVB1YpV 3-8 Y?$fa##Mj4SY~ŵB6954n_sI5T𒸧ҁ29E,3j*4#>2(K 7rU3F'o$%JcmڀmeN]!~omqI +7} -OmrǼWyd(vSGV Z W[!+EkN;,i`WU Xy6Va m~0. ݐH|#YduEVО juCt8{"G:vvPTBznc /`x"R +uX\4<=VLdEE٤Y +f"Htqߣ-ϵE= +>U+͡c =}O?-PE)!֋{CMeKXfeW:$J犆jl4+^Jg2<)%@A38la<F.g}zvwS*}8L|̔yf :A +gށJsZt:>DzMʥ1i{BPp<:t?5,9U*Ƀտ'BsUxUZ@C-HP$}x_'lA gBA !Z7WAX YX{ =ob1iEЛT䩄'!: +ԋy{V 9NhR,8PzU2y_1]hK{ܯ$ϧΣNHTqWX፜?Q!{M0ˌڀjOX%+h{ШG}&#B8*Ϫ*ȴy۔^>Eok}afIKkۓ*Yc~|hk]IOvZnXk;nXOAJoK~42::C"_pܽYG"4TL^7 EYmܥu]/omhq0=OM ,<hxepr,AN b#AӁ@u l SzT9V`gZ*}Pգ^}ݫ'i#1N3G$ .>|@W +(ZX#QG}:ZrT׶˟JO;<ʏ^ƸV-ndt2+y:'`n؝~}|e. *<1noY:[k wJ, endstream endobj -352 0 obj +358 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 351 0 R +/Contents 357 0 R /Resources 4 0 R -/Annots [ 353 0 R 354 0 R 355 0 R 356 0 R 357 0 R 358 0 R 359 0 R 360 0 R 361 0 R 362 0 R ] +/Annots [ 359 0 R 360 0 R 361 0 R 362 0 R 363 0 R 364 0 R 365 0 R 366 0 R 367 0 R 368 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -353 0 obj +359 0 obj << /Type /Annot /Subtype /Link @@ -5403,7 +5425,7 @@ endobj >> >> endobj -354 0 obj +360 0 obj << /Type /Annot /Subtype /Link @@ -5418,7 +5440,7 @@ endobj >> >> endobj -355 0 obj +361 0 obj << /Type /Annot /Subtype /Link @@ -5433,7 +5455,7 @@ endobj >> >> endobj -356 0 obj +362 0 obj << /Type /Annot /Subtype /Link @@ -5448,7 +5470,7 @@ endobj >> >> endobj -357 0 obj +363 0 obj << /Type /Annot /Subtype /Link @@ -5463,7 +5485,7 @@ endobj >> >> endobj -358 0 obj +364 0 obj << /Type /Annot /Subtype /Link @@ -5478,7 +5500,7 @@ endobj >> >> endobj -359 0 obj +365 0 obj << /Type /Annot /Subtype /Link @@ -5493,7 +5515,7 @@ endobj >> >> endobj -360 0 obj +366 0 obj << /Type /Annot /Subtype /Link @@ -5508,7 +5530,7 @@ endobj >> >> endobj -361 0 obj +367 0 obj << /Type /Annot /Subtype /Link @@ -5523,7 +5545,7 @@ endobj >> >> endobj -362 0 obj +368 0 obj << /Type /Annot /Subtype /Link @@ -5538,30 +5560,31 @@ endobj >> >> endobj -363 0 obj +369 0 obj << /Filter /FlateDecode -/Length 826 +/Length 841 >> stream -x͎6 ~ -@K$$h3zhMe٭gv0 ֆ,[ͿeA|q1dZꞻTr#MZzf`+XHH_Nݯ]}_欦=bbR-R?p\w'  _3]:wkmO~0Ka~z?|}BBq.¥#f:h%/Z(x[]\x+9Iѹ\#_ͱkW}Skܜt8&54w7q - yyQI\ч~ɔ{FNPD#|=Ro $makmk=ZE.ԫxYKgց GjFx?^1K[ݓ$Ųv@x7ZC6X֩UaK\EY&נD0x2;A)}8)1(U=V-A)+Ez;_$ қX%.Wgʹ;?/}5[eXgjJBq12'88dx?^-+)#hﱖga>󲗒UBËfg?R͑7ٽ9_z8XRUR(PJʾSi4:#j)6n\@+lAY!\Ůyo!Bnu_YF%8DC8iK%f΀J%٧oV +xVn6 WVKHAEöS@у/zhl(eى'x3Ɔg$2#)aq1dZ:< @\Eg~4K9'6CN"dA$L}9~=}))v9'A/&z|c)5W7{ry46gq3P쮙87o30Qi(F`^8k?36{F +)P;.nNLYID1w*㻒sDo/g pͱWg-[g{O<oxZH5|b2zeG-;ry kFSq:R#W%.E[$J83hvR<xOI'%Vkj qlX:Mu|'_猯v_/Ʊg}bOmq"VB'Bv74z:rY |TN=ħCc_IJ-V'b}xNƇ~b=mPTI(yKZy/x(cWLȾi :PB-aK~!P>Rv K*V\o + wpw˶^ڎ}z endstream endobj -364 0 obj +370 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 363 0 R +/Contents 369 0 R /Resources 4 0 R -/Annots [ 365 0 R 366 0 R 367 0 R 368 0 R 369 0 R 370 0 R 371 0 R 372 0 R 373 0 R 374 0 R 375 0 R 376 0 R 377 0 R 378 0 R ] +/Annots [ 371 0 R 372 0 R 373 0 R 374 0 R 375 0 R 376 0 R 377 0 R 378 0 R 379 0 R 380 0 R 381 0 R 382 0 R 383 0 R 384 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -365 0 obj +371 0 obj << /Type /Annot /Subtype /Link @@ -5576,7 +5599,7 @@ endobj >> >> endobj -366 0 obj +372 0 obj << /Type /Annot /Subtype /Link @@ -5591,7 +5614,7 @@ endobj >> >> endobj -367 0 obj +373 0 obj << /Type /Annot /Subtype /Link @@ -5606,7 +5629,7 @@ endobj >> >> endobj -368 0 obj +374 0 obj << /Type /Annot /Subtype /Link @@ -5621,7 +5644,7 @@ endobj >> >> endobj -369 0 obj +375 0 obj << /Type /Annot /Subtype /Link @@ -5636,7 +5659,7 @@ endobj >> >> endobj -370 0 obj +376 0 obj << /Type /Annot /Subtype /Link @@ -5651,7 +5674,7 @@ endobj >> >> endobj -371 0 obj +377 0 obj << /Type /Annot /Subtype /Link @@ -5666,7 +5689,7 @@ endobj >> >> endobj -372 0 obj +378 0 obj << /Type /Annot /Subtype /Link @@ -5681,7 +5704,7 @@ endobj >> >> endobj -373 0 obj +379 0 obj << /Type /Annot /Subtype /Link @@ -5696,7 +5719,7 @@ endobj >> >> endobj -374 0 obj +380 0 obj << /Type /Annot /Subtype /Link @@ -5711,7 +5734,7 @@ endobj >> >> endobj -375 0 obj +381 0 obj << /Type /Annot /Subtype /Link @@ -5726,7 +5749,7 @@ endobj >> >> endobj -376 0 obj +382 0 obj << /Type /Annot /Subtype /Link @@ -5741,7 +5764,7 @@ endobj >> >> endobj -377 0 obj +383 0 obj << /Type /Annot /Subtype /Link @@ -5756,7 +5779,7 @@ endobj >> >> endobj -378 0 obj +384 0 obj << /Type /Annot /Subtype /Link @@ -5771,35 +5794,33 @@ endobj >> >> endobj -379 0 obj +385 0 obj << /Filter /FlateDecode -/Length 2208 +/Length 2254 >> stream -xZˎ+"Y|,&\P I0BR,H"AFw8uX,2~gͲ|"CC-rN%a^HZ 4ZKzKNJ@cKz -B.?r!".w@Z5˗?᏿ˏM/*_h)l= a]W5LZ|%{uemZ~S>׶}|v9t1|}}Cc4U6ۥ.;݊ӜC6myhDݓ -"[2v2gexS%[&weÆuBP|?(u0 JzR:KjJKѤx wdӈX<Rx`;6<5,vRq*@8e#]Z  f*B"؋] S6\'R RMl쎦ƒ?@~`,*Ua2q]oo6NE,Tq條q-h!{3Dpf={$CG`Ż%GB"BI5s Ufpq΀='XLqC,=XɬbkxuQlB:=~sꘟ'e}΋{HMAQBO̱qP tZtue"W,gLr5;ļሑs!hߧS$r̞C;r`Vd;eA#,F鍑e҃Ta UY+㶻:JXO׶Ռ{PQ$T!)y_̒'O#L׼|@uW242 ʏj4ڟd1` -^H,5Jywy%_B?D4*#1,Q%V_ɚFbBґXu =͒9|o2m<9XSʡE,U -uËain QER=2|=V<2Þԫeߠ2Ѓ}ՈqC9rTP+gspFIЫ*^^Lf0&ᭃul"Jt8{hQKhU 鏸E^uhf$/OYܷHk“ q\^=ļʩQVfDE0@*a{/2'ҁ7'-Z/Gni DkóP%51u0*m*-Ӊ u{W)\Y_q00 ѾOU͟4"^&zMkMAT=rןoػ[u9hޢ4jqk+W'r~{5̛^*@׻pM6!(>x?1n)H{NʰII*4yrQѸ -}3GwڳHs:!|B6 Y& mx9|؄Wc qS&o(QwcmC-.|>MA!{ճщA;mB)0ؖ=Ltᤡ>?j`RN]nw=}>Prz;]MPQ6N[*Xy /F5KV;a -"_x`CkCmwO]T@"kj#dgWP>;ٜZq)J24:] Ɔ?ME${n8k/)-E;*g`|x hnv_-λ @)˗~'Z*Cn#E"6+Bk0ϗ/_xѦ/*?h_nw>r?EE^K>e ;}vI~n}]־}moT޲ai-f_opߐ|v3bl~Ku_6k+zN}*wߣMWK*ڂRnRZ"L!5ػJ6Ll'eÂ=u8CP|?(u0JzRkއFh |+[Gb]۵y}_cjlXN^An!=gS[җʹy=/xңOq+gQKjWҦ5OkҊdZiyǛiq%P5@xO9)C')zy|Ur!Yvo.8Qd !ʖW Y{kw +n0Y>0ImL;6n>pUGIPFގI\r/ 4RxQBo(Y#ze*30zeKM\qp^6? iky{o)5cAhK5Ega|31vW'^p7asg\Pȳߌ)2i{"rۗobHT/]soS;0: taA]/}T{Gԛo;U ~uBlPiq tՔ`]LNRG£b7BG1F#¨AMóM/z|\S0\ĥGNjּ|ڤ`~rjߞ6]Y;ԈGgS7 +rm%iqރθ|5J^Գe#5SM=HF;*8Qɩk:#Pn ԟÜEFx\",7Z >톬ón vGezkyY'N*xDϩhm +#/`xJCOUBc3h%\el#4Ea4h"I΁-!Ǜj,K㋃Fzpr'F',%m֡kvxmtrZ=]Бt{lY$!1pEK>x4ЦfB@'Ό%/;"g;#jȋ>e,T;yh04YlH^Bm# U'cETk'"lg7^FD]&g]9IvDb~%pŸIciaB)8!`6m`.Dj}Fc%'.^ &"2Zy7V5YM/ &ӥ@T"lA٘sX|z$3 -ΓP<5:Zj|`o8볶 endstream endobj -380 0 obj +386 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 379 0 R +/Contents 385 0 R /Resources 4 0 R -/Annots [ 381 0 R 382 0 R 383 0 R 384 0 R 385 0 R 386 0 R 387 0 R 388 0 R 389 0 R 390 0 R 391 0 R 392 0 R 393 0 R ] +/Annots [ 387 0 R 388 0 R 389 0 R 390 0 R 391 0 R 392 0 R 393 0 R 394 0 R 395 0 R 396 0 R 397 0 R 398 0 R 399 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -381 0 obj +387 0 obj << /Type /Annot /Subtype /Link @@ -5814,7 +5835,7 @@ endobj >> >> endobj -382 0 obj +388 0 obj << /Type /Annot /Subtype /Link @@ -5829,7 +5850,7 @@ endobj >> >> endobj -383 0 obj +389 0 obj << /Type /Annot /Subtype /Link @@ -5844,7 +5865,7 @@ endobj >> >> endobj -384 0 obj +390 0 obj << /Type /Annot /Subtype /Link @@ -5859,7 +5880,7 @@ endobj >> >> endobj -385 0 obj +391 0 obj << /Type /Annot /Subtype /Link @@ -5874,7 +5895,7 @@ endobj >> >> endobj -386 0 obj +392 0 obj << /Type /Annot /Subtype /Link @@ -5889,101 +5910,101 @@ endobj >> >> endobj -387 0 obj -<< -/Type /Annot -/Subtype /Link -/Rect [ 37.466457 314.429622 37.466457 302.026422 ] -/BS << -/W 0 ->> -/A << -/Type /Action -/S /URI -/URI (./Images/Screens/TyreZoneCropedWithoutBackGround.png) ->> ->> -endobj -388 0 obj -<< -/Type /Annot -/Subtype /Link -/Rect [ 37.466457 314.429622 57.716457 297.179622 ] -/BS << -/W 0 ->> -/A << -/Type /Action -/S /URI -/URI (./Images/Screens/TyreZoneCropedWithoutBackGround.png) ->> ->> -endobj -389 0 obj -<< -/Type /Annot -/Subtype /Link -/Rect [ 297.637795 297.179622 297.637795 284.776422 ] -/BS << -/W 0 ->> -/A << -/Type /Action -/S /URI -/URI (./Images/Screens/TyreZoneCropedWithoutBackGround.png) ->> ->> -endobj -390 0 obj -<< -/Type /Annot -/Subtype /Link -/Rect [ 37.466457 228.962022 37.466457 216.558822 ] -/BS << -/W 0 ->> -/A << -/Type /Action -/S /URI -/URI (./Images/Screens/TyreZoneFilter1.png) ->> ->> -endobj -391 0 obj -<< -/Type /Annot -/Subtype /Link -/Rect [ 37.466457 228.962022 57.716457 211.712022 ] -/BS << -/W 0 ->> -/A << -/Type /Action -/S /URI -/URI (./Images/Screens/TyreZoneFilter1.png) ->> ->> -endobj -392 0 obj -<< -/Type /Annot -/Subtype /Link -/Rect [ 297.637795 211.712022 297.637795 199.308822 ] -/BS << -/W 0 ->> -/A << -/Type /Action -/S /URI -/URI (./Images/Screens/TyreZoneFilter1.png) ->> ->> -endobj 393 0 obj << /Type /Annot /Subtype /Link -/Rect [ 37.466457 90.780822 37.466457 78.377622 ] +/Rect [ 37.466457 302.026422 37.466457 289.623222 ] +/BS << +/W 0 +>> +/A << +/Type /Action +/S /URI +/URI (./Images/Screens/TyreZoneCropedWithoutBackGround.png) +>> +>> +endobj +394 0 obj +<< +/Type /Annot +/Subtype /Link +/Rect [ 37.466457 302.026422 57.716457 284.776422 ] +/BS << +/W 0 +>> +/A << +/Type /Action +/S /URI +/URI (./Images/Screens/TyreZoneCropedWithoutBackGround.png) +>> +>> +endobj +395 0 obj +<< +/Type /Annot +/Subtype /Link +/Rect [ 297.637795 284.776422 297.637795 272.373222 ] +/BS << +/W 0 +>> +/A << +/Type /Action +/S /URI +/URI (./Images/Screens/TyreZoneCropedWithoutBackGround.png) +>> +>> +endobj +396 0 obj +<< +/Type /Annot +/Subtype /Link +/Rect [ 37.466457 216.558822 37.466457 204.155622 ] +/BS << +/W 0 +>> +/A << +/Type /Action +/S /URI +/URI (./Images/Screens/TyreZoneFilter1.png) +>> +>> +endobj +397 0 obj +<< +/Type /Annot +/Subtype /Link +/Rect [ 37.466457 216.558822 57.716457 199.308822 ] +/BS << +/W 0 +>> +/A << +/Type /Action +/S /URI +/URI (./Images/Screens/TyreZoneFilter1.png) +>> +>> +endobj +398 0 obj +<< +/Type /Annot +/Subtype /Link +/Rect [ 297.637795 199.308822 297.637795 186.905622 ] +/BS << +/W 0 +>> +/A << +/Type /Action +/S /URI +/URI (./Images/Screens/TyreZoneFilter1.png) +>> +>> +endobj +399 0 obj +<< +/Type /Annot +/Subtype /Link +/Rect [ 37.466457 78.377622 37.466457 65.974422 ] /BS << /W 0 >> @@ -5994,32 +6015,32 @@ endobj >> >> endobj -394 0 obj +400 0 obj << /Filter /FlateDecode /Length 1830 >> stream -xZۊ#7}WV CB&6LHBCw{! HI-K6=xRJS.m1py'0}|j8?烲z-<0pN8?X\*Ӡ×ï~[g!dZxt!(>w8Ey ^2_YapW4z+'qA --9d<̓9ͿQ6QwpkI49͕AxǔB~~;&}>xYJf, Rj&q6ÌgœfEÙ^O@8.(FqZnV"( ״[ƃBi ;R]UF{%+:J6B5(aTF{ѵQrOE"{>: 1˓:O;4ڧt,y|bo+2j'B4Y9$[{4яzV۝H9v' ֵ)MVg<ẂBS`sZ8)|+fC(#G6;S^AY$=&w+inPj2 7!?Z2< 1=߿zypLX~:jƱ$WVR2GJl5 ] _e 4!&õkb(_/!3eJiOaO,T)G*ZJ"{u~&wF3#EC8-2+T`l;:ئ5`7Āql ӂ+aՇ &2OnJsWl5?yx_yfo/j>\z3}huҫVåWKtHߧ@v\{F%[wj$1*w_ .1䱉.wi,^F6f1eb|pFA y ^2_YahD+:`ep8 ԖOOg~2b^ɜ`ߨ⛨;gh*sRaNsa10<_~>xO:9< KXɌRAJ$wvY"s +q. iV4e Sof%^A,pMe|J#H,Hؑ"rX5X QrY1hWË&꒾8&ؠ42Xɟ%$_DK^8i]t=}:Gk5ŋ>W!$e௏!rAhrIdi<;rI/AęD84#Q RѭQy!4S6v7AAȵiS,Րܬ @JsA|]2+sx#;I#($efNqJk2'"8my0=jQi&2EL1F.Ƚמ!єb5LixA!ӸtqYW{:xHf ׉P;dA:wxP4$Q8:xnMSA0'Mr%6i?q|&;x܊*;Yu E-a%M֣ƘUN0/6Nîݝ5M)-226#$sU TDmH1J0oJ+`ZqyCiO6LJ#!<0מX岝T1mz>fWy23k=tbK b6 QGl cw}c"x)$zLV$`SN6vXʇ:YC['k7EdP2~1i[d\2ZIͣ3R+-JJlx-XV97ЂPn5^hqB+8xBם-Ź>3]gÐ7$ \ Uaar+_ TVyhMkUWB]W롪1 2TTԃݑ.1wo=A8Uo)TosCֿG;<n8ѭÒ9 0IavKh=h;Tn[Nb)zS^"$͔IQtIl[mTsɜH&tOQL|[*ȫ/HU6)X$%?zDI8mw1T$/F.MiQhl !R8)f |&E3\g??Mm +B.y׮v{|ܓhg +{bNq$"oJ߯[aOɝ(%d~/NLJ<5XN6is  4q`jǴxӰKR$\I̓V|*~yƚ ugRKQ&k|-fg?k0d endstream endobj -395 0 obj +401 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 394 0 R +/Contents 400 0 R /Resources 4 0 R -/Annots [ 396 0 R 397 0 R 398 0 R 399 0 R 400 0 R ] +/Annots [ 402 0 R 403 0 R 404 0 R 405 0 R 406 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -396 0 obj +402 0 obj << /Type /Annot /Subtype /Link @@ -6034,7 +6055,7 @@ endobj >> >> endobj -397 0 obj +403 0 obj << /Type /Annot /Subtype /Link @@ -6049,7 +6070,7 @@ endobj >> >> endobj -398 0 obj +404 0 obj << /Type /Annot /Subtype /Link @@ -6064,7 +6085,7 @@ endobj >> >> endobj -399 0 obj +405 0 obj << /Type /Annot /Subtype /Link @@ -6079,7 +6100,7 @@ endobj >> >> endobj -400 0 obj +406 0 obj << /Type /Annot /Subtype /Link @@ -6094,32 +6115,34 @@ endobj >> >> endobj -401 0 obj +407 0 obj << /Filter /FlateDecode -/Length 1779 +/Length 1788 >> stream xZɎ7+Ls_cF  ȡ՚6rH;flRȶdZj.UYLoZ0zZ>}>q GQ}H?I9悑NA3ar3!TRh~;s]Pq7q˧Y0>}FVSLk Hԛ!7}Kp ~ˋFۋaU?/z^ÿ $ g +H9c`7\=I6H'gןL)xYB[@&ߤbJK-FÁIogεOQpn9\8c}JX#'4;Zv*^ %YXm!#njw *mL:%y͞`|ۼβIq3/BPq&ײ\(3˃PLIvXokq}Lc쮧t??ɜ<>}Ϝ1D}y< 6YHܱq88@؍^(33>4z;*LhzX(dܺ͏_U5:ϼ:xA"jTzrM*ƒ~!.%1 5c9``7 - -hMA=SJ(yr%YmA΃zm[p)dCǿ8bU Dp@BV<}AJmՏ0?J +!t/J('SjFރW;#gr9/ÓZEnlz4~`RӜ"뉿pd[Tbim_Zp/AM9+ƬAuub6$}-5#2ڲqc ;Th9P~ :y"dS@P!0%ƌ5VwGN8dp ʉg2ʿF%K$]Mځ7{TY-8^B)ImSHw^%41%!wE%.~n-^22Pkho(Ո|QR!$ U|1^^+\HpbR;~:}rPw,8"-!UyױaDzB9JQ]63z@JA=@'%WY;EŨe`g6e0}D|#_xG_⭴LxG5?@6@SmAGϻzt*)3M/zFWz*$%̃lPR]Nɼʺ!,Fz *0;D|>"jTzrM*ƒ~8i>'IPƚq.h[hc +]jBKZt3+@2Xݵm¶ ;Wk48iqF3CI}7 +)ҥr:!K3ITʹ]UaVhLUɺG +D4PWj5if(7xpY̯BF4nrJz(+% ǟKoר>L;}SU| |GU#t ~?uj u!m?\П)H9' L;F!@q)9gJi{ E9R-yQm9`ugX +>(V +GP;5 ĶCi0?tի endstream endobj -402 0 obj +408 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 401 0 R +/Contents 407 0 R /Resources 4 0 R -/Annots [ 403 0 R 404 0 R 405 0 R 406 0 R ] +/Annots [ 409 0 R 410 0 R 411 0 R 412 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -403 0 obj +409 0 obj << /Type /Annot /Subtype /Link @@ -6134,7 +6157,7 @@ endobj >> >> endobj -404 0 obj +410 0 obj << /Type /Annot /Subtype /Link @@ -6149,7 +6172,7 @@ endobj >> >> endobj -405 0 obj +411 0 obj << /Type /Annot /Subtype /Link @@ -6164,7 +6187,7 @@ endobj >> >> endobj -406 0 obj +412 0 obj << /Type /Annot /Subtype /Link @@ -6179,32 +6202,30 @@ endobj >> >> endobj -407 0 obj +413 0 obj << /Filter /FlateDecode -/Length 772 +/Length 802 >> stream -xV͎0)x=ǖpXT␦@Čc7N"Q؞ox - -')eL߇Y&'WXM<L eÐ,(ct?|~ -;lj D*F劉fï(fWt4\(&}L>4P`yNi>:'7rD?'~txc -xtG4o~3Y%g%Vb1Gab)Bdv>p1*e#/jv (D>dfEZ3s2*vμEgiε`s XuF9\QPU]/aϛ 'mx`%@?ɆNfl+nnYN{g:TM0cixՎNmZ8:\=O%ZIuKa:u*'zh&Kф\1D`:8;bV{AJlg -D^+†_Tؒ^*nI9پ> ΋\'EyaeވЂ] B%}5H8`V'y`14rɖ۱Z薃zr"O,MpNSZ>I-ATrKz"3d0l.Lwų^3y:֏SHq^ Bx$vNVt3J^|jH]`h{ڈ9 ' IeR̋FF!eOb!K"+ :gS5~xyD 0i9zdo3>K-(h3,tօTȍ;Mλ;&r$G=Uj62N)@ƀ9h޿tc,QȤNohnVN&VyvQ`. Dz&5{ЭG'pT;*Gd+cc9Ϻj|_e,o `y-%&ϸ&$8;вn| >\ui}]/TKvL1g;Fό- *:~J%xcy~pu%X@+ w];~U٭S]ֶ<}s!Hdt5t˴6&2FdL@INR ))$&CֹAC :[~Ē&4 gׇN9-2z}9p@Ha> endobj -409 0 obj +415 0 obj << /Type /Annot /Subtype /Link @@ -6219,7 +6240,7 @@ endobj >> >> endobj -410 0 obj +416 0 obj << /Type /Annot /Subtype /Link @@ -6234,67 +6255,224 @@ endobj >> >> endobj -411 0 obj +417 0 obj << /Filter /FlateDecode -/Length 2309 +/Length 2338 >> stream -xZKo$5ϯ?X  NfaA vw"M2_}Ubx-Y./9OO_/1tv i bF+KλE)zK*_>}Ǎw!_Y\"^E"*n)-NOw?~XgJ٤C|5W/qXoe1m}53K plK!)gQϏWZ ǂWQ@ -7es4L؁5 nX$LeϨCZ!l `ƞ &X`.Ia8zexBm&f2T=5 |ZV$GvRB5o;V9*gC#ohsMSan"5iG_3mXE=7#bӠYK9% Ge !qtkٱ`$Lq/h*0/!#Y|3buhۀm{2p(SE} -D ?f91|difadL@NJ$f^yO \'AA`/0w.(V0[hsʻ] ."~sEx)ݘ9GVV9sd6k)J^CFTG7Tǡ&Ď6*L KAҌAq&3e ( 1Ԟx\!yLt7b X[V^WcѿЬ"']G8_ۅ҆uYz5 -!d(3ƌ&Z:96d:R}}P݈$ćY9֔7$?V>zݎ+ݼdG>(Q5Fvt% vd< ~$fNa<`Άa%p0A0N-% A@=d8c\Cb5B[^?{A\ ĄFfAg.M[L] E ~6_Kw0_t_˞c6|\PpB MrŔinx?.#)bCJM˦TPf5SGh%8b6f٥(]EO''rRyx$A@C8L]S8%f'A8klgHҺne$.x>CAjFRW1;کҍCI_!o!]=a;PXHBfjujOzH6H="CVJuMl+ ߑ -$e< Vk10>>Ɏ:m\ vf_ls#e hjzr`p!Z]cdRA(_9gJR[-C?!G? -6{9r.z6rgSzq-t鹸+I/R. :R{m;g6* @mMrۭ ->=SN8BcȐp9Yf:tav +xZK5ϯ?CZ "ޝC@ _vrݳK8dga󫯪[,<~#^ }pV/ۧ g?/1tv i bF+KλE)zK*_>}ōw!_Y\"^E"[&U-NOw?~XgJ٤C5>Y9>FϷ;OxL\ck,rnۏk}f?ƷFe}5B[[S^L6Вq\xgQw[exmv^!+?~]ůE%pm`owe +yUЋv*Fw|v;7 +ch}ZK<X̞&&fL*4e4 7"-d9nPTt-X7#`%}mǡЄc~/0d5>5g{@樼77}FANbYnyj| u؅[kMYG6AW?´`ɑHNđ6nMGnz~vXuQ0 q*Y{h|^ YqX gS2mǠ,_ܖBSͣ)ȹ+>: +/P؁5 b,qYNpgԉ!-Ljrr$`@32h +&c&zȆ~1dMdzXk4-$lHk+6w2,r(3ƌ&Z: 96d龕w}P݈$sćY9֔+Du+GǕzn[ rD#;MR:ʂZZq2NwOyЄ? +0}0g0OM8 `sJ g2Fy k1܊B!!ښ$^=C2Cm(1!`򹓩YxKOZL] E ~o%8 a53+Q]˞c6|\Ppi@A򰑾6 t:|$U,#zHi CB[X~ٕ̼fyuk[2:#fcC[trZzG\* 6HHupHgI~r +YRNvk)[ ,=P%@ڿzUu vjf{U!$޷Á(|0+xN=BOGdJI.Iec 0;XbN;keXaO2N$\8o;sAJ-WDžl4^PvhL5e`fΨtD&#Bo1ȁe.Dle&r"~d퓊.;Ŭs%k,NE7TYyPIA u?Az +.E -9ԧ 7Lv=}<0W{ +_fЫ5k v"{p-PmR?sw T#a z.~T"!E`RPD'8D z$Z;m704Y_^Dzu(RI12Zas]Fw ?CVz0]gY# endstream endobj -412 0 obj +418 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 411 0 R +/Contents 417 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -413 0 obj +419 0 obj << /Filter /FlateDecode -/Length 7898 +/Length 10736 >> stream -xoƕ+$`4&$ĀIvl9$SqvK'[d}\+sr?\+qu_m{<zz\u -6rie7|߇Y.viُ6q~zY⃞>J~^J~̴rl9ʴ@R@fzԗg(^T^c,v˫j4mf7Gߍqڇ(Թeo -do-!y= rm]<p{}|Kր ܽKB~,K Adž3{sAǾ- AY0-r s{Hu Ag/[HG_r.v9/oC2/K5!˾/qYֱ# f ൶=ú Ӛ \KșuAZאl]F:#{!1y;TDV,D1Hedsi\AsTI 1,A̵n[H [*&kA؏ 憫rcLZ)52%KSsC{KQsd.2o}bsG{NQsߗ-1 1+6ļ bƞ!潯SuKLG"J󱝱tČG1h)7:b>#flה?;b>Sn bFj)v<8r,`J[#AҧDr,2}<Ȗ[=ߝkd3N]pbmA\7 fLRmA 1-2yCuHeCDD'1BlkT[6A|,Ah1H>:U{bQ|br}p=1Ll 5`r}5sb60@D ڗ fGA$k |A([Aԗ5AԗrD1)7Q_\0H>bmA -As?}2wP;z bk-}p(RA7Xȟ>ڂS  bn!е-p>3ڂ";! - %Xp5>X@iGk E5>{l}iQA=vڂ>X*As?胥RnUFG&0ܨ胸zZ#S'LkD}bKʐ۞֞+ %Ֆ:~/%UA=Ֆ>{LkDH.0)BnTAl,`0S%Ֆ>oi"T[*`9q3G1HĔ?IL>XN킜sEDNT[* rHkD}k-}o#Cb9@ Aֈ* g<,^RJ-xZQRnFTkSlAk 5"<9Pڂ>X+fAQV, }yVcmADiֆV}6";)kGJ>ZQCKzi"z-m/2`VČ -֟q׀[Z#ju?F cM&dtd9@ȟ>8SmicY45zbri -s s? }׾q{Z#j胸gZn胘ج4`ļ{Znci9:& kKii⮨4A?c ->޺챶6ִF bmAhi2^_JkD }@9@b1iX[qFqk-8{Z#Msi;)1k `ۑiA;RWRm85uA=Ֆ>PQG#vۉ9@z] }D1qj-}稧5>stҷ!]d60} FGF1֟)jK/LkD}ojKGXH bސ)1% Vjds  vڂsĠ9@ A=uAĽ5>K-}qF֟;`oe}5"B{-能gZ#胈iKkD}1X[ӑֈ:`wZ9@ Vֈ:`1H>QG絴Fy{-9KZ8{-8gZ#8֚j :^ю>cihGēj )` !09@|y[+ 3 = ah_q&[;lF"#?wM0GMkD; eۙֈv{Z#­@ A1Ms&_g׉}e|q}4F86e~?K(iJ%WݍqEo2m~aEB%PB%PB$PB%PB%HB%PB%PB$PB%PB$PB%PB%LJ`(J`(g$J`(J`(xFJ`(J`(g$J`(J`(xFJ`(J`(g$J`(J`(xFJ`(J`(g$J`(J`(xFJ`(J`(g$J`(J`(XfPC %0PC %0P<# %0PC %0PC 3PC %0PC %0P<# %0PC %0PC 3PC %0PC %0P<# %0PC %0PC 3PC %0PC %0P<# %0PC %0PC 3PC %0PC %0P<# %0PC %0PC 2J(J(I(J(J(J(J(I(J(J(J(J(I(J(J(J(J(I(J(J(J(J(I(J(J(;%PB %PB %HB %PB %PB$PB %PB %HB %PB %PB$PB %PB %HB %PB %PB$PB %PB %HB %PB %PB$PB %PB %HB %PB %PB$J`(J`(xFJ`(J`(g$J`(J`(xFJ`(J`(g$J`(J`(xFJ`(J`(g$J`(J`(xFJ`(J`(g$J`(J`(xFJ`(J`(e& %0PC %0PC 3PC %0PC %0P<# %0PC %0PC 3PC %0PC %0P<# %0PC %0PC 3PC %0PC %0P<# %0PC %0PC 3PC %0PC %0P<# %0PC %0PC 3PC %0PC %0P,3I(J(J(J(J(I(J(J(J(J(I(b{7FB1OP -R~1ds3 kƻ+Bicf|ݞ/~1c܍a]{afnO=tnvO!8{eSÕqsSyx}xnvpOyfUޭȽGXT~k*3R|K* -wyrt3r&Z{E4' f+:ß?a.<ͅvyXS+OW=_y}ynY^A㼤=mw,_O]l}}ۧc_Oܗ_=۟ǽ~z]?e/7m<׋|c9T3p?2Q{r{j?_?җy{>gg5{4{FcV8^Uy~)ϱ|5Pfc#D?cu>/x"}~popC|KS=8TS?%?2{D SwO~?n/cRqUo+w$wm?kI{BIZ|}f~3w.KI<@j()_<|/TgS\_̿ӯoxnpSyh, -uj,-<|nϣn+ztj/#gyu37cN'ow?;<6~뺎1NoTc -tn;^_UlCx^t3Ws5? +x[#W^}4N(HpAb @'iKme~NtA3aHZ功7߾}?uynlmכݼ]MX.˾~8Ӭ|i<?wo|نa۷8M6N1?ߍ׻^~U-q/y?[Ѯ|8<_{ӟi3sqeޯχy=%!rLëZ?>^ǟ8˰ ?F5.6zuI?;Pvij_.ôýtǟcwF1hwmZ#2icƑ&D]u;2ѳ5_n?fnsF >ei0D4}3DݗcZh6 D4XaFkǸ0v X{ap}8;̗cqɠ|ڲњ9l /xD7LӁFtxi  y<8hӸ0#tu9<5h>"2jh4yi'ynÁFcyBXi%'yƼ,g5emƼێFcnzinm4Ϣ17mo47yyh̫gƼnDiڕiYƼmG1떕OӘuEEf_&2ƼkD4Vy\[VYƆFc޵X5]=Mc֙HgӘ476Y;Fc־hlז}ۈfѤ|Ьٹӊk@hz?G?\[ m4Nhm@.Úh̓?:himj'1:3ime>? Y[-JmE1/цs9-h4f̟ItsCZFdjkL#YAGA=q9Z˦ƼAGֲ֖Iฟ㮏4{[im:%9ḣhh:djGt2sCh4h>8 :}Pkd&>5hhz4jNh24A/#-;Y/FsC֗јg>8sõE4<Ϭ}PkLky}Gk:ј֖YhB1/zjhmS,Fcn<Ѯ-7Z[t ;]imɈh̫`hmWV23DFct@sCƼ47NҥYग C>2kաY@G֖Ytl3] Jkx%Yޫ]#>F׈f탳>`4?gڢ}pYFY!s>Ѭ}PÅdMfpm>8MH׈f탳&]k}Gk,qm>YѬ}Pﱍ;pm>8/:@1/:}p^hnh<5"}d9̍E?+]#ҧNkDA-y@F༶=/hmY 3]#Z꽱ڲh{kD\2:2:hEF0탚3-AF׈탚;-Л|h">8zinhtќ}PsڲhԜѢ}PGZ[G4x~<A׉-5_]ևy)`55e=sth>KGkAM…S̾g_-Th\I>x^µE;kD/؛e?5\[jmth>4m4.Mh4U4./5FFԴs>9҆ :E1kzl5vtH/#-m<ߤFcui8hmiˢti\499̟}PLkK>~kDM^?7:imi۠(48ntiN>ŴCh4C1zZ[VzVFjstڲNwڡdtg>:jѪ}P)d֖֕u>th>NDk˪}p+d4IA=-=yA1:}PEh\g>pkDA{kDA{e>q7FjԸwj\<ڢ}pՎK׈t*mh\q5U45Upm>1th>:j̛hnh\U5UA]#Zmh\:5"-2LןW~h|5UkE;̺5MkkDA}hmGJetG2:oMckDA fϛAwE'52B׈6Jk6mt5M5wFn9 ۬s27g.s0Bj}f|ڎ}e@mSQ}~6^o|ϋP$Ϋcw?Kw1v=_عG(8BJ&P2qIE(8BJ&P2qIE(8BJ&P2qIE(8BJ&P"%G(8BJ&P""0P8B#l*B#0P8Bɦ"0P8B#l*B#0P8Bɦ"0P8B#l*B#0P8Bɦ"0P8B#l*B#0P8Bɦ"0P8B#l*B#0P8BP8B#0P#0P8B%P8B#0P#0P8B%P8B#0P#0P8B%P8B#0P#0P8B%P8B#0P#0P8B%.B#0P8Bɦ"0P8B#l*B#0P8Bɦ"0P8B#l*B#0P8Bɦ"0P8B#l*B#0P8Bɦ"0P8B#l*B#0P8Bɦ"0P8B#hn +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`DE(`q +G(T +G(`qME(`q +G(T +G(`qME(`q +G(T +G(`qME(`q +G(T +G(`qME(`q +G(T +G(`qMq +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ4] +G(`qME(`q +G(T +G(`qME(`q +G(TJ4mqW_#OگWdx4Cq~~^ρF;.˰%W4Ucw11ǟc|aJ$DRJ$DRJ"]IE(TIE(THDRJ$DRJ$$E(TIE(THDRJ$DRJ$$G(TME(TME(tJ6dSJ6dSJ4]ME(TME(TMdSJ6dSJ6DE(TME(TME(tJ6dSJ6dSJ4]ME(TME(TMdSJ6dSJ6DE(TME(TME(tJ6dSJ6dSJ2}ME(TME(TMdSJ6dSJ6DE(TME(TME(tJ6dSJ6dSJ4]ME(TME(TMdSJ6dSJ6DE(TME(TME(tJ6dSJ6dSJ4]ME(TME(TMdSJ6dSJ6$G(TME(TME(tJ6dSJ6dSJ4]ME(TME(TMdSJ6dSJ6DE(TME(TME(tJ6dSJ6dSJ4]ME(TME(TMdSJ6dSJ6DE(TME(TME(tJ6dSJ6dSJ2mG(TME(TME(tJ6dSJ6dSJ4]ME(TME(TMdSJ6dSJ6DE(TME(TME(tJ6dSJ6dSJ4]ME(TME(TMdSJ6dSJ6DE(TME(TME(tJ6dSJ6dSJ2}ME(TME(TMdSJ6dSJ6DE(TME(TME(tJ6dSJ6dSJ4]ME(TME(TMdSJ6dSJ6DE(TME(TME(tJ6dSJ6dSJ4]ME(TME(TMdSJ6dSJ6$G(TME(TME(tJ6dSJ6dSJ4]ME(TME(TMdSJ6dSJ6DE(TME(TME(tJ6dSJ6dSJ4]ME(TME(TMdSJ6dSJ6DE(TME(TME(tJ6dSJ6dSJ2}ME(TME(TMdSJ6dSJ6DE(TME(TME(tJ6dSJ6dSJ4]ME(ɼΏWD(m,:P;@ς|J?3BKҫX{??ƺ=BJ&P2q#H*BJ&P2q#H*BJ&P2q#H*BJ&P2qIE(8BJ&P2qIq +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ4] +G(`qME(`q +G(T +G(`qME(`q +G(T +G(`qME(`q +G(T +G(`qME(`q +G(T +G(`qME(`q +G(t +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`DE(`q +G(T +G(`qME(`q +G(T +G(`qME(`q +G(T +G(`qME(`q +G(T +G(`qME(`q +G(T +G(`qMq +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ4] +G(`qME(`q +G(T +G(`qME(`q +G(T +G(`qME(`q +G(T +G(`qME(`q +G(T +G(`qME(`q +G(t +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(ѼBiU"Uw[Fw?Kwjͽ,G(|WNzcw?Kw1v=_عG(8BJ&P2qIE(8BJ&P2qIE(8BJ&P2qIE(8BJ&P"%G(8BJ&=BP8B#0=Bɦ"0P8B~P#0P8B#l*B#0P%P8B#0=Bɦ"0P8B~P#0P8B#l*B#0P8Bɦ"0P8B#l*B#0P8BP8B#0P#0P8B%P8B#0P#0P8B%P8B#0P#0P8B%P8B#0P#0P8B%P8B#0P#0P8B%.B#0P8Bɦ"0P8B#l*B#0P8Bɦ"0P8B#l*B#0P8Bɦ"0P8B#l*B#0P8Bɦ"0P8B#l*B#0P8Bɦ"0P8B#h#0P8B%P8B#0P#0P8B%P8B#0P#0P8B%P8B#0P#0P8B%P8B#0P#0P8B%P8B#0P"0P8B#l*B#0P8Bɦ"0P8B#l*B#0P8Bɦ"0P8B#l*B#0P8Bɦ"0P8B#l*B#0P8Bɦ"0P8B#l*B#0P8BP8B#0P#0P8B%P8B#0P#0P8B%P8B#0P#0P8B%P8B#0P#0P8B%P8B#0P#0P8B%.B#0P8Bɦ"0P8B#l*B#0P8Bɦ"0P8B#l*B%W>|~EecQ #>p/!%zI6??LJS~cn{sP2q#Ld%P2q#Ld%P2q#Ld%P2q#LDRJ&P2q#LDE(`q +G(T +G(`qME(`q +G(T +G(`qME(`q +G(T +G(`qME(`q +G(T +G(`qME(`q +G(T +G(`qMq +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ4] +G(`qME(`q +G(T +G(`qME(`q +G(T +G(`qME(`q +G(T +G(`qME(`q +G(T +G(`qME(`q +G(t +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`DE(`q +G(T +G(`qME(`q +G(T +G(`qME(`q +G(T +G(`qME(`q +G(T +G(`qME(`q +G(T +G(`qMq +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ6q +G(`dS +G(`qJ4] +G(`qME(`q +G(T +G(`qME(`q +G(TJ42Wn{;<\Z۳_ߟ˯gݟ__;=bf7nv2[la7U1Ý~Mݴ)޲[#ۖ̚x87 .23'NL={7xJ{x-8ovW{'f;wSlwK~4:<[Ny}[ sß9~~|j y9 L7L_l^ +îz.:>O qk>lg^8Xs}$͟~}Kn{Y:Eq}=gy힥-gr29[|\^/9?/^vZv/}}m1>^OϿ/C?]E? ̟߿a<os[tK=Ò~fϼeҿħ~E{{s=|-v~ɯO~O˟ggUZϯ ˲o^uiomInK織>2nxijy/|ce9_b?glGiחS-o< ?ߒMˢఔ[tHo/ +,R--~ϰTﺬqI6=.y$әmiLyYòy6OmL3p[>?qV _u28ՒC!u '*{|ןw>{׷ S7ܽۯ?ܗy}L矿??~ +g~~Ǐ~x:g8Ol/~g2]cn/4ɼX-~ùi|.&嗹Xf}8/#[s7JoT?g|a,W@-e]u!^`zu}_kn=}Ȼ 7W]hJ%q%!_y#Eyfo{w)/Ù~k(%LM_zJշ~wjc=YOimޟlOqK`Ɲ> Z]?Np}ٺvzã~#_j;'=a}ezg^v~n<>ݞIz N;8= njsX_N|'7Ws--Iϳw B2 endstream endobj -414 0 obj +420 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 413 0 R +/Contents 419 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -415 0 obj +421 0 obj << /Filter /FlateDecode /Length 4225 @@ -6316,19 +6494,19 @@ C rJQQ[}?Z핻؟+] o,0?7=䯘YA7c~2ذS<0怎e.S:nKbkMsOCq<]lI3c/w9XXG7Í'BI/%%/|!>g{_/Lj]O÷g4B.?&#iVm";g\*bt׮껵k~VMϷ?˙lK s0^'LǬw)z?ǻRg0?B1r[wY$0ߖ6{j.?-^auAxz{Är endstream endobj -416 0 obj +422 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 415 0 R +/Contents 421 0 R /Resources 4 0 R -/Annots [ 417 0 R 418 0 R 419 0 R 420 0 R 421 0 R ] +/Annots [ 423 0 R 424 0 R 425 0 R 426 0 R 427 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -417 0 obj +423 0 obj << /Type /Annot /Subtype /Link @@ -6343,7 +6521,7 @@ endobj >> >> endobj -418 0 obj +424 0 obj << /Type /Annot /Subtype /Link @@ -6358,7 +6536,7 @@ endobj >> >> endobj -419 0 obj +425 0 obj << /Type /Annot /Subtype /Link @@ -6373,7 +6551,7 @@ endobj >> >> endobj -420 0 obj +426 0 obj << /Type /Annot /Subtype /Link @@ -6388,7 +6566,7 @@ endobj >> >> endobj -421 0 obj +427 0 obj << /Type /Annot /Subtype /Link @@ -6403,7 +6581,7 @@ endobj >> >> endobj -422 0 obj +428 0 obj << /Filter /FlateDecode /Length 5455 @@ -6426,19 +6604,19 @@ b Ƈ/ endstream endobj -423 0 obj +429 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 422 0 R +/Contents 428 0 R /Resources 4 0 R -/Annots [ 424 0 R ] +/Annots [ 430 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -424 0 obj +430 0 obj << /Type /Annot /Subtype /Link @@ -6453,7 +6631,7 @@ endobj >> >> endobj -425 0 obj +431 0 obj << /Filter /FlateDecode /Length 1606 @@ -6471,19 +6649,19 @@ k}d }$]OzHlLqGxPql7gCh9 cj7Y0J_CաOT<w:Dt endstream endobj -426 0 obj +432 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 425 0 R +/Contents 431 0 R /Resources 4 0 R -/Annots [ 427 0 R 428 0 R ] +/Annots [ 433 0 R 434 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -427 0 obj +433 0 obj << /Type /Annot /Subtype /Link @@ -6498,7 +6676,7 @@ endobj >> >> endobj -428 0 obj +434 0 obj << /Type /Annot /Subtype /Link @@ -6513,7 +6691,7 @@ endobj >> >> endobj -429 0 obj +435 0 obj << /Filter /FlateDecode /Length 4124 @@ -6535,19 +6713,19 @@ C ֋tZkKf&?T~&_߉gm̬U_m=U3יfM9r}Yii3K~᧡ra3ZJmc le/9?=s\؇x/?5{s߻R=Vtoy(oC5^S.!Ȫ;RZsvWDzV`,EQKS<ǿWB=ҷl_Z>G縴x愹]&QvxM]]=cΧO/8KFKbC2e?qKF{뷗nrd&(,rcmhڹ s."wU~٦>zEXjG#Ά-_Mq[T*߽M[޽Ma[T޽M[ߖJ޽M}[l߻[l>=m_vo~}Ar 5ZUͼuVz/E,P:J_>AOt-{V{5w3f\o>|{Ӟg%ko鲻vÄ? endstream endobj -430 0 obj +436 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 429 0 R +/Contents 435 0 R /Resources 4 0 R -/Annots [ 431 0 R 432 0 R 433 0 R ] +/Annots [ 437 0 R 438 0 R 439 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -431 0 obj +437 0 obj << /Type /Annot /Subtype /Link @@ -6562,7 +6740,7 @@ endobj >> >> endobj -432 0 obj +438 0 obj << /Type /Annot /Subtype /Link @@ -6577,7 +6755,7 @@ endobj >> >> endobj -433 0 obj +439 0 obj << /Type /Annot /Subtype /Link @@ -6592,7 +6770,7 @@ endobj >> >> endobj -434 0 obj +440 0 obj << /Filter /FlateDecode /Length 4728 @@ -6614,18 +6792,18 @@ bw FHsuk4# šڟ\lhZO r||-МcfƹҝJ]wBί[R`wƆ endstream endobj -435 0 obj +441 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 434 0 R +/Contents 440 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -436 0 obj +442 0 obj << /Filter /FlateDecode /Length 4333 @@ -6648,18 +6826,18 @@ D L AgЀb{\9iK_=9}3y60 ӼHaS-kd_O|{f!uʏU~o0/h5 -wo7rO_rd-ȱѿz&)w~o 9{ZzL$TQܙ.K)f2jp{iٺ%M:OHX%f*_[ٶJ_q5/0s/uCOS-\Ad??W;3+ 3>mLոY,ѿfwٍ}ú1a]?AHwXO$.q<7x[/\}Q,}c.o];#\W.4pFcE;GcqN٧_8 yƽrwm<z endstream endobj -437 0 obj +443 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 436 0 R +/Contents 442 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -438 0 obj +444 0 obj << /Filter /FlateDecode /Length 4363 @@ -6678,19 +6856,19 @@ L [$o[OdTUIA@FH2`}[5eW9ܑ8+_㲎cojIP' p'V~`-lZĺB:_kd3#0ke걬ڿ|lc=?_+BH} i6WH,&:;rmsWwȻ;f Y6z董?gK- A3{R?PA6ܰ69l+ܓyio}ޏ9Slj׋l I#!]&@cAe%a+(1"eZu-ǡ2d"T }W仰qȚ׊Ͱzŀ0?2}Jw ^f mU?Zz<(dw#A,ٖT?J+oY9!g n,ե6'Pp(<{2'{"G V{- m+m`nw=|ܞ}W;xa\-zoTbl0zκ6#^f}r m{&:LjʸlɪZǕ?v9Yo7$8-M!#;m</cxr[IY<*WY؝o5r/>ϟߑ-esZԊD㎤X_V;.~n_gk>$߿TwvW)[gLϵo>o} wU=8!_?u2Nb? ˸ova~_p endstream endobj -439 0 obj +445 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 438 0 R +/Contents 444 0 R /Resources 4 0 R -/Annots [ 440 0 R 441 0 R 442 0 R ] +/Annots [ 446 0 R 447 0 R 448 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -440 0 obj +446 0 obj << /Type /Annot /Subtype /Link @@ -6705,7 +6883,7 @@ endobj >> >> endobj -441 0 obj +447 0 obj << /Type /Annot /Subtype /Link @@ -6720,7 +6898,7 @@ endobj >> >> endobj -442 0 obj +448 0 obj << /Type /Annot /Subtype /Link @@ -6735,7 +6913,7 @@ endobj >> >> endobj -443 0 obj +449 0 obj << /Filter /FlateDecode /Length 385 @@ -6745,18 +6923,18 @@ x Q>I%R@n9#%m뺮>!lBbq -v S>g3 (m[gMVep$;y W/PvᙡSB/o߹߯ bc 톷 endstream endobj -444 0 obj +450 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 443 0 R +/Contents 449 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -445 0 obj +451 0 obj << /Filter /FlateDecode /Length 400 @@ -6766,18 +6944,18 @@ x U-|rYL<'|[^[X!,(mz#c-0c*=3>۩JS2UF:1-GfF-,re2\T-Gv^"TSOu7UIlRO/^#:&5ܒ,.`xpp)[zqWjcTVQGqjwV%jA^yU?|nM5j endstream endobj -446 0 obj +452 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 445 0 R +/Contents 451 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -447 0 obj +453 0 obj << /Filter /FlateDecode /Length 386 @@ -6786,18 +6964,18 @@ stream xR͎1 )xNlib9,\8tA,vs(c Ʉ[| )Wa~zNܠV FDX8.A[~>) YDWAc`~"ђꗈoIkӗE%) ؔ8D>6OpRTYK=l^x:<oE΃LŨ2VrZFGx <o޾xx<=OTC)7Q QC%r?xE$|"Fj>ֺt7K%fCt[U-Ph?`Jh}6+Tv럱C[pvU]%x*;kYykv\G!?̸; endstream endobj -448 0 obj +454 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 447 0 R +/Contents 453 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -449 0 obj +455 0 obj << /Filter /FlateDecode /Length 2608 @@ -6819,19 +6997,19 @@ $% ӡ  endstream endobj -450 0 obj +456 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 449 0 R +/Contents 455 0 R /Resources 4 0 R -/Annots [ 451 0 R 452 0 R 453 0 R 454 0 R 455 0 R 456 0 R 457 0 R ] +/Annots [ 457 0 R 458 0 R 459 0 R 460 0 R 461 0 R 462 0 R 463 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -451 0 obj +457 0 obj << /Type /Annot /Subtype /Link @@ -6846,7 +7024,7 @@ endobj >> >> endobj -452 0 obj +458 0 obj << /Type /Annot /Subtype /Link @@ -6861,7 +7039,7 @@ endobj >> >> endobj -453 0 obj +459 0 obj << /Type /Annot /Subtype /Link @@ -6876,7 +7054,7 @@ endobj >> >> endobj -454 0 obj +460 0 obj << /Type /Annot /Subtype /Link @@ -6891,7 +7069,7 @@ endobj >> >> endobj -455 0 obj +461 0 obj << /Type /Annot /Subtype /Link @@ -6906,7 +7084,7 @@ endobj >> >> endobj -456 0 obj +462 0 obj << /Type /Annot /Subtype /Link @@ -6921,7 +7099,7 @@ endobj >> >> endobj -457 0 obj +463 0 obj << /Type /Annot /Subtype /Link @@ -6936,7 +7114,7 @@ endobj >> >> endobj -458 0 obj +464 0 obj << /Filter /FlateDecode /Length 2654 @@ -6958,19 +7136,19 @@ mb 韯Lm!̔;%}e56/Cj-''kD\ atUP;c+ tR endstream endobj -459 0 obj +465 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 458 0 R +/Contents 464 0 R /Resources 4 0 R -/Annots [ 460 0 R 461 0 R 462 0 R ] +/Annots [ 466 0 R 467 0 R 468 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -460 0 obj +466 0 obj << /Type /Annot /Subtype /Link @@ -6985,7 +7163,7 @@ endobj >> >> endobj -461 0 obj +467 0 obj << /Type /Annot /Subtype /Link @@ -7000,7 +7178,7 @@ endobj >> >> endobj -462 0 obj +468 0 obj << /Type /Annot /Subtype /Link @@ -7015,7 +7193,7 @@ endobj >> >> endobj -463 0 obj +469 0 obj << /Filter /FlateDecode /Length 626 @@ -7026,19 +7204,19 @@ x Vjr;shBG.͠J8^ r-|'yUI9|Rw?Gv endstream endobj -464 0 obj +470 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 463 0 R +/Contents 469 0 R /Resources 4 0 R -/Annots [ 465 0 R 466 0 R 467 0 R ] +/Annots [ 471 0 R 472 0 R 473 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -465 0 obj +471 0 obj << /Type /Annot /Subtype /Link @@ -7053,7 +7231,7 @@ endobj >> >> endobj -466 0 obj +472 0 obj << /Type /Annot /Subtype /Link @@ -7068,7 +7246,7 @@ endobj >> >> endobj -467 0 obj +473 0 obj << /Type /Annot /Subtype /Link @@ -7083,7 +7261,7 @@ endobj >> >> endobj -468 0 obj +474 0 obj << /Filter /FlateDecode /Length 734 @@ -7093,19 +7271,19 @@ x N'5JyMV^wXYa<{d?aqc7GFe2zI<8h6=~TdK`%(5bHz'5P.\hh٧,@p9ۤPᳬ*l endstream endobj -469 0 obj +475 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 468 0 R +/Contents 474 0 R /Resources 4 0 R -/Annots [ 470 0 R 471 0 R 472 0 R ] +/Annots [ 476 0 R 477 0 R 478 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -470 0 obj +476 0 obj << /Type /Annot /Subtype /Link @@ -7120,7 +7298,7 @@ endobj >> >> endobj -471 0 obj +477 0 obj << /Type /Annot /Subtype /Link @@ -7135,7 +7313,7 @@ endobj >> >> endobj -472 0 obj +478 0 obj << /Type /Annot /Subtype /Link @@ -7150,7 +7328,7 @@ endobj >> >> endobj -473 0 obj +479 0 obj << /Filter /FlateDecode /Length 737 @@ -7163,19 +7341,19 @@ L ēY/.ؔ-0:GgRNAfm^]s/.hY,_Zׅ?#Jd9KˠZ;^i3dA4|wdO endstream endobj -474 0 obj +480 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 473 0 R +/Contents 479 0 R /Resources 4 0 R -/Annots [ 475 0 R 476 0 R 477 0 R ] +/Annots [ 481 0 R 482 0 R 483 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -475 0 obj +481 0 obj << /Type /Annot /Subtype /Link @@ -7190,7 +7368,7 @@ endobj >> >> endobj -476 0 obj +482 0 obj << /Type /Annot /Subtype /Link @@ -7205,7 +7383,7 @@ endobj >> >> endobj -477 0 obj +483 0 obj << /Type /Annot /Subtype /Link @@ -7220,7 +7398,7 @@ endobj >> >> endobj -478 0 obj +484 0 obj << /Filter /FlateDecode /Length 751 @@ -7232,19 +7410,19 @@ x nrWwXi| 8@D3pUG$*@&zL*3hVX~ St`qEy>5U쫪*u͝ۻg/jWdS) dR9btd4"L޲~IvOW&/WPtz7(GI(6+ zkEKST(( F;qW4׳t!QKUr#)@f]H]ڜ<R_ ̢ endstream endobj -479 0 obj +485 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 478 0 R +/Contents 484 0 R /Resources 4 0 R -/Annots [ 480 0 R 481 0 R 482 0 R 483 0 R 484 0 R 485 0 R ] +/Annots [ 486 0 R 487 0 R 488 0 R 489 0 R 490 0 R 491 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -480 0 obj +486 0 obj << /Type /Annot /Subtype /Link @@ -7259,7 +7437,7 @@ endobj >> >> endobj -481 0 obj +487 0 obj << /Type /Annot /Subtype /Link @@ -7274,7 +7452,7 @@ endobj >> >> endobj -482 0 obj +488 0 obj << /Type /Annot /Subtype /Link @@ -7289,7 +7467,7 @@ endobj >> >> endobj -483 0 obj +489 0 obj << /Type /Annot /Subtype /Link @@ -7304,7 +7482,7 @@ endobj >> >> endobj -484 0 obj +490 0 obj << /Type /Annot /Subtype /Link @@ -7319,7 +7497,7 @@ endobj >> >> endobj -485 0 obj +491 0 obj << /Type /Annot /Subtype /Link @@ -7334,7 +7512,7 @@ endobj >> >> endobj -486 0 obj +492 0 obj << /Filter /FlateDecode /Length 1436 @@ -7353,19 +7531,19 @@ c ͸7orÙNÿn endstream endobj -487 0 obj +493 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 486 0 R +/Contents 492 0 R /Resources 4 0 R -/Annots [ 488 0 R 489 0 R ] +/Annots [ 494 0 R 495 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -488 0 obj +494 0 obj << /Type /Annot /Subtype /Link @@ -7380,7 +7558,7 @@ endobj >> >> endobj -489 0 obj +495 0 obj << /Type /Annot /Subtype /Link @@ -7395,7 +7573,7 @@ endobj >> >> endobj -490 0 obj +496 0 obj << /Filter /FlateDecode /Length 1283 @@ -7409,19 +7587,19 @@ Z r>{3V}Sɇd?G{%BHӕ^Wy:=Yyu):DQ:V.͙pI^k=a.RH1+WrYJ^N[;wO'@1`h@kmPa<.RR_ś~yj|_V1W;ħ5jf\?s]DTZ I+«(C$&%x92ߘ(̝31 jtC7j:4lpZDٛ?;z3Fm ĄPZHUǛt_}|}vu,w{@#@Lpkb]/l.eLEZn~ |riDiD8G-b/nkeiXs#$y endstream endobj -491 0 obj +497 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 490 0 R +/Contents 496 0 R /Resources 4 0 R -/Annots [ 492 0 R 493 0 R 494 0 R ] +/Annots [ 498 0 R 499 0 R 500 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -492 0 obj +498 0 obj << /Type /Annot /Subtype /Link @@ -7436,7 +7614,7 @@ endobj >> >> endobj -493 0 obj +499 0 obj << /Type /Annot /Subtype /Link @@ -7451,7 +7629,7 @@ endobj >> >> endobj -494 0 obj +500 0 obj << /Type /Annot /Subtype /Link @@ -7466,7 +7644,7 @@ endobj >> >> endobj -495 0 obj +501 0 obj << /Filter /FlateDecode /Length 992 @@ -7479,19 +7657,19 @@ x &Sjf/T{*Zf{f{cQvRsJ rN9^?e%ـuglܤPI\uPtRzDp9}E~O endstream endobj -496 0 obj +502 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 495 0 R +/Contents 501 0 R /Resources 4 0 R -/Annots [ 497 0 R 498 0 R 499 0 R 500 0 R ] +/Annots [ 503 0 R 504 0 R 505 0 R 506 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -497 0 obj +503 0 obj << /Type /Annot /Subtype /Link @@ -7506,7 +7684,7 @@ endobj >> >> endobj -498 0 obj +504 0 obj << /Type /Annot /Subtype /Link @@ -7521,7 +7699,7 @@ endobj >> >> endobj -499 0 obj +505 0 obj << /Type /Annot /Subtype /Link @@ -7536,7 +7714,7 @@ endobj >> >> endobj -500 0 obj +506 0 obj << /Type /Annot /Subtype /Link @@ -7551,7 +7729,7 @@ endobj >> >> endobj -501 0 obj +507 0 obj << /Filter /FlateDecode /Length 1178 @@ -7569,19 +7747,19 @@ NSH ^NL?Fă꡸jeSxNJ21 endstream endobj -502 0 obj +508 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 501 0 R +/Contents 507 0 R /Resources 4 0 R -/Annots [ 503 0 R 504 0 R 505 0 R ] +/Annots [ 509 0 R 510 0 R 511 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -503 0 obj +509 0 obj << /Type /Annot /Subtype /Link @@ -7596,7 +7774,7 @@ endobj >> >> endobj -504 0 obj +510 0 obj << /Type /Annot /Subtype /Link @@ -7611,7 +7789,7 @@ endobj >> >> endobj -505 0 obj +511 0 obj << /Type /Annot /Subtype /Link @@ -7626,7 +7804,7 @@ endobj >> >> endobj -506 0 obj +512 0 obj << /Filter /FlateDecode /Length 873 @@ -7639,19 +7817,19 @@ x E "o.[m "cȚ;;2uv߯޺I̻L[K/'#1㐭 ` 2yЂgiqcI ºF/C€|9ā2E5G^^X,W +<똿V3~ǂ>^/'j嬶=qNbwh(DD̉ŕgKe47mS߈ZzKhJ@R@mie~)3'q8> endobj -508 0 obj +514 0 obj << /Type /Annot /Subtype /Link @@ -7666,7 +7844,7 @@ endobj >> >> endobj -509 0 obj +515 0 obj << /Type /Annot /Subtype /Link @@ -7681,7 +7859,7 @@ endobj >> >> endobj -510 0 obj +516 0 obj << /Type /Annot /Subtype /Link @@ -7696,7 +7874,7 @@ endobj >> >> endobj -511 0 obj +517 0 obj << /Filter /FlateDecode /Length 798 @@ -7706,19 +7884,19 @@ x o6.3u3t:@㼨?3BVv*޼W,8FFRA[,F ޽VЈ>?M_ mWa]T ACd.*j ]N,C#NTmsy?9$Yq$xz endstream endobj -512 0 obj +518 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 511 0 R +/Contents 517 0 R /Resources 4 0 R -/Annots [ 513 0 R 514 0 R 515 0 R ] +/Annots [ 519 0 R 520 0 R 521 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -513 0 obj +519 0 obj << /Type /Annot /Subtype /Link @@ -7733,7 +7911,7 @@ endobj >> >> endobj -514 0 obj +520 0 obj << /Type /Annot /Subtype /Link @@ -7748,7 +7926,7 @@ endobj >> >> endobj -515 0 obj +521 0 obj << /Type /Annot /Subtype /Link @@ -7763,7 +7941,7 @@ endobj >> >> endobj -516 0 obj +522 0 obj << /Filter /FlateDecode /Length 1166 @@ -7780,19 +7958,19 @@ N oUļ ֣*IA,F:t[Z@rVfУ>9Ͷ2bO^Cnlys/5ro9uJ#nodGh'C'r#3;Sr+O=|u 1@{pv!㑧 @m(r(Hm|0ak0Ӝ1mBOc* ^Gܔ-}Ptf?ߜduE} 7.TP 1 !-̻Ke@WY_w\ endstream endobj -517 0 obj +523 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 516 0 R +/Contents 522 0 R /Resources 4 0 R -/Annots [ 518 0 R 519 0 R ] +/Annots [ 524 0 R 525 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -518 0 obj +524 0 obj << /Type /Annot /Subtype /Link @@ -7807,7 +7985,7 @@ endobj >> >> endobj -519 0 obj +525 0 obj << /Type /Annot /Subtype /Link @@ -7822,7 +8000,7 @@ endobj >> >> endobj -520 0 obj +526 0 obj << /Filter /FlateDecode /Length 1632 @@ -7840,19 +8018,19 @@ x ۤ 6 endstream endobj -521 0 obj +527 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 520 0 R +/Contents 526 0 R /Resources 4 0 R -/Annots [ 522 0 R 523 0 R 524 0 R 525 0 R 526 0 R 527 0 R ] +/Annots [ 528 0 R 529 0 R 530 0 R 531 0 R 532 0 R 533 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -522 0 obj +528 0 obj << /Type /Annot /Subtype /Link @@ -7867,7 +8045,7 @@ endobj >> >> endobj -523 0 obj +529 0 obj << /Type /Annot /Subtype /Link @@ -7882,7 +8060,7 @@ endobj >> >> endobj -524 0 obj +530 0 obj << /Type /Annot /Subtype /Link @@ -7897,7 +8075,7 @@ endobj >> >> endobj -525 0 obj +531 0 obj << /Type /Annot /Subtype /Link @@ -7912,7 +8090,7 @@ endobj >> >> endobj -526 0 obj +532 0 obj << /Type /Annot /Subtype /Link @@ -7927,7 +8105,7 @@ endobj >> >> endobj -527 0 obj +533 0 obj << /Type /Annot /Subtype /Link @@ -7942,7 +8120,7 @@ endobj >> >> endobj -528 0 obj +534 0 obj << /Filter /FlateDecode /Length 2141 @@ -7960,19 +8138,19 @@ P X«ׇ9qfC~:^|ND©7̝3VE_]z}0ҫSM^xåW'.:rhـK^?\z뷧^|-9^}xm|rq>IQ"Yp\/%oB`ʇ\nXk4KsJ_-#cVj޸N&+Or^v|^c`ZG_C9NTWYPJi;T> endobj -530 0 obj +536 0 obj << /Type /Annot /Subtype /Link @@ -7987,7 +8165,7 @@ endobj >> >> endobj -531 0 obj +537 0 obj << /Type /Annot /Subtype /Link @@ -8002,7 +8180,7 @@ endobj >> >> endobj -532 0 obj +538 0 obj << /Type /Annot /Subtype /Link @@ -8017,7 +8195,7 @@ endobj >> >> endobj -533 0 obj +539 0 obj << /Type /Annot /Subtype /Link @@ -8032,7 +8210,7 @@ endobj >> >> endobj -534 0 obj +540 0 obj << /Filter /FlateDecode /Length 805 @@ -8044,19 +8222,19 @@ z D9~9E\K)ru'C,8A$}qû V+#])LgЙ䊌)Sr=ԄD[#\B[E$ iYkD[X_.sHNFn[C-G/)j=:_ٷRN9K׿(|޵%lr 8Q#Br* (1_ Vw.J',eപ {piW }X3sUszp^0On Gٿ ,( endstream endobj -535 0 obj +541 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 534 0 R +/Contents 540 0 R /Resources 4 0 R -/Annots [ 536 0 R 537 0 R ] +/Annots [ 542 0 R 543 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -536 0 obj +542 0 obj << /Type /Annot /Subtype /Link @@ -8071,7 +8249,7 @@ endobj >> >> endobj -537 0 obj +543 0 obj << /Type /Annot /Subtype /Link @@ -8086,7 +8264,7 @@ endobj >> >> endobj -538 0 obj +544 0 obj << /Filter /FlateDecode /Length 2513 @@ -8106,18 +8284,18 @@ TOC ̊?L endstream endobj -539 0 obj +545 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 538 0 R +/Contents 544 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -540 0 obj +546 0 obj << /Filter /FlateDecode /Length 2399 @@ -8136,19 +8314,19 @@ d~K .|%܊"ຍ{[0M5JH\b*@LSy6E”'j5?&mhN^ endstream endobj -541 0 obj +547 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 540 0 R +/Contents 546 0 R /Resources 4 0 R -/Annots [ 542 0 R 543 0 R 544 0 R 545 0 R ] +/Annots [ 548 0 R 549 0 R 550 0 R 551 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -542 0 obj +548 0 obj << /Type /Annot /Subtype /Link @@ -8163,7 +8341,7 @@ endobj >> >> endobj -543 0 obj +549 0 obj << /Type /Annot /Subtype /Link @@ -8178,7 +8356,7 @@ endobj >> >> endobj -544 0 obj +550 0 obj << /Type /Annot /Subtype /Link @@ -8193,7 +8371,7 @@ endobj >> >> endobj -545 0 obj +551 0 obj << /Type /Annot /Subtype /Link @@ -8208,7 +8386,7 @@ endobj >> >> endobj -546 0 obj +552 0 obj << /Filter /FlateDecode /Length 905 @@ -8222,19 +8400,19 @@ x tZq endstream endobj -547 0 obj +553 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 546 0 R +/Contents 552 0 R /Resources 4 0 R -/Annots [ 548 0 R 549 0 R 550 0 R 551 0 R 552 0 R 553 0 R ] +/Annots [ 554 0 R 555 0 R 556 0 R 557 0 R 558 0 R 559 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -548 0 obj +554 0 obj << /Type /Annot /Subtype /Link @@ -8249,7 +8427,7 @@ endobj >> >> endobj -549 0 obj +555 0 obj << /Type /Annot /Subtype /Link @@ -8264,7 +8442,7 @@ endobj >> >> endobj -550 0 obj +556 0 obj << /Type /Annot /Subtype /Link @@ -8279,7 +8457,7 @@ endobj >> >> endobj -551 0 obj +557 0 obj << /Type /Annot /Subtype /Link @@ -8294,7 +8472,7 @@ endobj >> >> endobj -552 0 obj +558 0 obj << /Type /Annot /Subtype /Link @@ -8309,7 +8487,7 @@ endobj >> >> endobj -553 0 obj +559 0 obj << /Type /Annot /Subtype /Link @@ -8324,7 +8502,7 @@ endobj >> >> endobj -554 0 obj +560 0 obj << /Filter /FlateDecode /Length 796 @@ -8335,19 +8513,19 @@ x BrU6v zܧ`#OdzLGS ޽ XCvn>_i_1UK`C!@Hy9joWyNOq2\ mgh~/rsM&oX\c|.$XlX1N d#։.$9ҖFw,ޓd8̎Ҁuuа܊~Z6nZ2nMOh~<+b{_/Fh 7s*|;+ѢZj.WaecR6PIy>P=s Q$/ȥ ǖ#R U_*mXL_R?:.@`\CoIŖ kZ\LpxIn!PV(CڲY8fsR][c

    > endobj -556 0 obj +562 0 obj << /Type /Annot /Subtype /Link @@ -8362,7 +8540,7 @@ endobj >> >> endobj -557 0 obj +563 0 obj << /Type /Annot /Subtype /Link @@ -8377,7 +8555,7 @@ endobj >> >> endobj -558 0 obj +564 0 obj << /Type /Annot /Subtype /Link @@ -8392,7 +8570,7 @@ endobj >> >> endobj -559 0 obj +565 0 obj << /Type /Annot /Subtype /Link @@ -8407,7 +8585,7 @@ endobj >> >> endobj -560 0 obj +566 0 obj << /Type /Annot /Subtype /Link @@ -8422,7 +8600,7 @@ endobj >> >> endobj -561 0 obj +567 0 obj << /Type /Annot /Subtype /Link @@ -8437,7 +8615,7 @@ endobj >> >> endobj -562 0 obj +568 0 obj << /Filter /FlateDecode /Length 2372 @@ -8454,19 +8632,19 @@ F_n֕ GB8! )K}_wKosgz ( PNh(n7hv1| U{Ă l^q d׎L9J0"/OL(%֗JP !<%=K8[ endstream endobj -563 0 obj +569 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 562 0 R +/Contents 568 0 R /Resources 4 0 R -/Annots [ 564 0 R 565 0 R 566 0 R 567 0 R 568 0 R ] +/Annots [ 570 0 R 571 0 R 572 0 R 573 0 R 574 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -564 0 obj +570 0 obj << /Type /Annot /Subtype /Link @@ -8481,7 +8659,7 @@ endobj >> >> endobj -565 0 obj +571 0 obj << /Type /Annot /Subtype /Link @@ -8496,7 +8674,7 @@ endobj >> >> endobj -566 0 obj +572 0 obj << /Type /Annot /Subtype /Link @@ -8511,7 +8689,7 @@ endobj >> >> endobj -567 0 obj +573 0 obj << /Type /Annot /Subtype /Link @@ -8526,7 +8704,7 @@ endobj >> >> endobj -568 0 obj +574 0 obj << /Type /Annot /Subtype /Link @@ -8541,7 +8719,7 @@ endobj >> >> endobj -569 0 obj +575 0 obj << /Filter /FlateDecode /Length 1975 @@ -8558,19 +8736,19 @@ x w )R}gU(=^SE~~Np\31U7C]N]Ek+K82H0"4R؁rrX_Ԅ997ͯrsԆi?޴5FhOe]#Ȭj>; anเ/wH簖pu\hB.rV" Il M! 8K SGǭ0t^Ǟ!|AA YK K\Ůp?b=` %JT[Ad`Io-]@dF?;hė,?ˋj%W*# ʚ9LL BB&ɒ@`թvQR_ohZ0@t$ C;-La>Y-.tﲛ{ S>QayÜLc 裃xQGc{ԍ ‘ endstream endobj -570 0 obj +576 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 569 0 R +/Contents 575 0 R /Resources 4 0 R -/Annots [ 571 0 R 572 0 R 573 0 R 574 0 R 575 0 R 576 0 R 577 0 R ] +/Annots [ 577 0 R 578 0 R 579 0 R 580 0 R 581 0 R 582 0 R 583 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -571 0 obj +577 0 obj << /Type /Annot /Subtype /Link @@ -8585,7 +8763,7 @@ endobj >> >> endobj -572 0 obj +578 0 obj << /Type /Annot /Subtype /Link @@ -8600,7 +8778,7 @@ endobj >> >> endobj -573 0 obj +579 0 obj << /Type /Annot /Subtype /Link @@ -8615,7 +8793,7 @@ endobj >> >> endobj -574 0 obj +580 0 obj << /Type /Annot /Subtype /Link @@ -8630,7 +8808,7 @@ endobj >> >> endobj -575 0 obj +581 0 obj << /Type /Annot /Subtype /Link @@ -8645,7 +8823,7 @@ endobj >> >> endobj -576 0 obj +582 0 obj << /Type /Annot /Subtype /Link @@ -8660,7 +8838,7 @@ endobj >> >> endobj -577 0 obj +583 0 obj << /Type /Annot /Subtype /Link @@ -8675,7 +8853,7 @@ endobj >> >> endobj -578 0 obj +584 0 obj << /Filter /FlateDecode /Length 958 @@ -8687,19 +8865,19 @@ x {ζTTLwlLzؾbft{ևYvӾokx)7%Ź9뢄`,E 2khsgMR%u :x#eYVs2-N Co NtnP6n}'SHf{idpGAt B\ JxԧSOnrʰ8,G"7#R> endobj -580 0 obj +586 0 obj << /Type /Annot /Subtype /Link @@ -8714,7 +8892,7 @@ endobj >> >> endobj -581 0 obj +587 0 obj << /Type /Annot /Subtype /Link @@ -8729,7 +8907,7 @@ endobj >> >> endobj -582 0 obj +588 0 obj << /Type /Annot /Subtype /Link @@ -8744,7 +8922,7 @@ endobj >> >> endobj -583 0 obj +589 0 obj << /Type /Annot /Subtype /Link @@ -8759,7 +8937,7 @@ endobj >> >> endobj -584 0 obj +590 0 obj << /Type /Annot /Subtype /Link @@ -8774,7 +8952,7 @@ endobj >> >> endobj -585 0 obj +591 0 obj << /Filter /FlateDecode /Length 1903 @@ -8787,19 +8965,19 @@ x +$`~1nh@sZ<p'\rgDYŷЈ~ S qC6%%VhY;DwL đA'_6O]M>eTb:"$ ԣ-B r}!^L 91 Cq&.]Eud@_G6CZ iY gX!t"kx{q"B\O}mJ͕ƠN@qOu?WYuWP9ο,r6XRuxT.dG9"wtk'ɾza" ~ endstream endobj -586 0 obj +592 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 585 0 R +/Contents 591 0 R /Resources 4 0 R -/Annots [ 587 0 R 588 0 R 589 0 R 590 0 R ] +/Annots [ 593 0 R 594 0 R 595 0 R 596 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -587 0 obj +593 0 obj << /Type /Annot /Subtype /Link @@ -8814,7 +8992,7 @@ endobj >> >> endobj -588 0 obj +594 0 obj << /Type /Annot /Subtype /Link @@ -8829,7 +9007,7 @@ endobj >> >> endobj -589 0 obj +595 0 obj << /Type /Annot /Subtype /Link @@ -8844,7 +9022,7 @@ endobj >> >> endobj -590 0 obj +596 0 obj << /Type /Annot /Subtype /Link @@ -8859,7 +9037,7 @@ endobj >> >> endobj -591 0 obj +597 0 obj << /Filter /FlateDecode /Length 1445 @@ -8876,19 +9054,19 @@ vT d* wcXӄU9x6g0/}uObAO{fn[skc`f}WBt> endobj -593 0 obj +599 0 obj << /Type /Annot /Subtype /Link @@ -8903,7 +9081,7 @@ endobj >> >> endobj -594 0 obj +600 0 obj << /Type /Annot /Subtype /Link @@ -8918,7 +9096,7 @@ endobj >> >> endobj -595 0 obj +601 0 obj << /Type /Annot /Subtype /Link @@ -8933,7 +9111,7 @@ endobj >> >> endobj -596 0 obj +602 0 obj << /Type /Annot /Subtype /Link @@ -8948,7 +9126,7 @@ endobj >> >> endobj -597 0 obj +603 0 obj << /Type /Annot /Subtype /Link @@ -8963,7 +9141,7 @@ endobj >> >> endobj -598 0 obj +604 0 obj << /Filter /FlateDecode /Length 2497 @@ -8985,19 +9163,19 @@ Dd ?a $CԗW"oލț"oIߊy%vʉx4^Fv < 7 J+N7!|qml(}oH! 喃d߼݊ۏYy?- endstream endobj -599 0 obj +605 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 598 0 R +/Contents 604 0 R /Resources 4 0 R -/Annots [ 600 0 R ] +/Annots [ 606 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -600 0 obj +606 0 obj << /Type /Annot /Subtype /Link @@ -9012,7 +9190,7 @@ endobj >> >> endobj -601 0 obj +607 0 obj << /Filter /FlateDecode /Length 1328 @@ -9028,19 +9206,19 @@ r YI;Sh7KJta7aO&$ endstream endobj -602 0 obj +608 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 601 0 R +/Contents 607 0 R /Resources 4 0 R -/Annots [ 603 0 R 604 0 R ] +/Annots [ 609 0 R 610 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -603 0 obj +609 0 obj << /Type /Annot /Subtype /Link @@ -9055,7 +9233,7 @@ endobj >> >> endobj -604 0 obj +610 0 obj << /Type /Annot /Subtype /Link @@ -9070,7 +9248,7 @@ endobj >> >> endobj -605 0 obj +611 0 obj << /Filter /FlateDecode /Length 2521 @@ -9096,18 +9274,18 @@ k8|L^1 Cr- *R+ c߹HShiG$4КB/t- endstream endobj -606 0 obj +612 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 605 0 R +/Contents 611 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -607 0 obj +613 0 obj << /Filter /FlateDecode /Length 1861 @@ -9123,19 +9301,19 @@ F bz怄xc endstream endobj -608 0 obj +614 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 607 0 R +/Contents 613 0 R /Resources 4 0 R -/Annots [ 609 0 R 610 0 R 611 0 R 612 0 R 613 0 R 614 0 R 615 0 R 616 0 R 617 0 R 618 0 R ] +/Annots [ 615 0 R 616 0 R 617 0 R 618 0 R 619 0 R 620 0 R 621 0 R 622 0 R 623 0 R 624 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -609 0 obj +615 0 obj << /Type /Annot /Subtype /Link @@ -9150,7 +9328,7 @@ endobj >> >> endobj -610 0 obj +616 0 obj << /Type /Annot /Subtype /Link @@ -9165,7 +9343,7 @@ endobj >> >> endobj -611 0 obj +617 0 obj << /Type /Annot /Subtype /Link @@ -9180,7 +9358,7 @@ endobj >> >> endobj -612 0 obj +618 0 obj << /Type /Annot /Subtype /Link @@ -9195,7 +9373,7 @@ endobj >> >> endobj -613 0 obj +619 0 obj << /Type /Annot /Subtype /Link @@ -9210,7 +9388,7 @@ endobj >> >> endobj -614 0 obj +620 0 obj << /Type /Annot /Subtype /Link @@ -9225,7 +9403,7 @@ endobj >> >> endobj -615 0 obj +621 0 obj << /Type /Annot /Subtype /Link @@ -9240,7 +9418,7 @@ endobj >> >> endobj -616 0 obj +622 0 obj << /Type /Annot /Subtype /Link @@ -9255,7 +9433,7 @@ endobj >> >> endobj -617 0 obj +623 0 obj << /Type /Annot /Subtype /Link @@ -9270,7 +9448,7 @@ endobj >> >> endobj -618 0 obj +624 0 obj << /Type /Annot /Subtype /Link @@ -9285,7 +9463,7 @@ endobj >> >> endobj -619 0 obj +625 0 obj << /Filter /FlateDecode /Length 781 @@ -9300,19 +9478,19 @@ x 2TpM{]Ϩ|,Z2 endstream endobj -620 0 obj +626 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 619 0 R +/Contents 625 0 R /Resources 4 0 R -/Annots [ 621 0 R 622 0 R 623 0 R ] +/Annots [ 627 0 R 628 0 R 629 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -621 0 obj +627 0 obj << /Type /Annot /Subtype /Link @@ -9327,7 +9505,7 @@ endobj >> >> endobj -622 0 obj +628 0 obj << /Type /Annot /Subtype /Link @@ -9342,7 +9520,7 @@ endobj >> >> endobj -623 0 obj +629 0 obj << /Type /Annot /Subtype /Link @@ -9357,7 +9535,7 @@ endobj >> >> endobj -624 0 obj +630 0 obj << /Filter /FlateDecode /Length 1251 @@ -9371,19 +9549,19 @@ L r;rhBgFe/^Ub#kwRp_[eKq3ghu8n:l\`҈ XN *.rkuʌ+n'qpY7ҽ97{K#DjU>b! .ع#V~ge,\49+=Nȟ$58JY'o.%8A\jA}>pHH.x>!] _"= endstream endobj -625 0 obj +631 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 624 0 R +/Contents 630 0 R /Resources 4 0 R -/Annots [ 626 0 R 627 0 R ] +/Annots [ 632 0 R 633 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -626 0 obj +632 0 obj << /Type /Annot /Subtype /Link @@ -9398,7 +9576,7 @@ endobj >> >> endobj -627 0 obj +633 0 obj << /Type /Annot /Subtype /Link @@ -9413,7 +9591,7 @@ endobj >> >> endobj -628 0 obj +634 0 obj << /Filter /FlateDecode /Length 3155 @@ -9428,18 +9606,18 @@ Q HZR[ͨHQA&Mq,Lz!4W5ҞZF'YF Nb I6QθY'=~AQ9;:j#C8"2!ISN1 ]\Wqw s$89(|T{3 فpa: Xs"4cCGY%[:0y˞9|C ecTysDR~,/oXR=-vNdI)<~/Mr@3PRug.:(ʽ37Unmrr~a$pwI)H)Z^/%n/fW endstream endobj -629 0 obj +635 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 628 0 R +/Contents 634 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -630 0 obj +636 0 obj << /Filter /FlateDecode /Length 3077 @@ -9462,18 +9640,18 @@ g B. %*(=E7M`)>p.ڔ\BIjSpr RHȢ ?זy endstream endobj -631 0 obj +637 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 630 0 R +/Contents 636 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -632 0 obj +638 0 obj << /Filter /FlateDecode /Length 1641 @@ -9489,19 +9667,19 @@ b]F GqT'gH*v+cNL`Uy endstream endobj -633 0 obj +639 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 632 0 R +/Contents 638 0 R /Resources 4 0 R -/Annots [ 634 0 R 635 0 R 636 0 R 637 0 R 638 0 R 639 0 R 640 0 R 641 0 R 642 0 R 643 0 R 644 0 R 645 0 R 646 0 R ] +/Annots [ 640 0 R 641 0 R 642 0 R 643 0 R 644 0 R 645 0 R 646 0 R 647 0 R 648 0 R 649 0 R 650 0 R 651 0 R 652 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -634 0 obj +640 0 obj << /Type /Annot /Subtype /Link @@ -9516,7 +9694,7 @@ endobj >> >> endobj -635 0 obj +641 0 obj << /Type /Annot /Subtype /Link @@ -9531,7 +9709,7 @@ endobj >> >> endobj -636 0 obj +642 0 obj << /Type /Annot /Subtype /Link @@ -9546,7 +9724,7 @@ endobj >> >> endobj -637 0 obj +643 0 obj << /Type /Annot /Subtype /Link @@ -9561,7 +9739,7 @@ endobj >> >> endobj -638 0 obj +644 0 obj << /Type /Annot /Subtype /Link @@ -9576,7 +9754,7 @@ endobj >> >> endobj -639 0 obj +645 0 obj << /Type /Annot /Subtype /Link @@ -9591,7 +9769,7 @@ endobj >> >> endobj -640 0 obj +646 0 obj << /Type /Annot /Subtype /Link @@ -9606,7 +9784,7 @@ endobj >> >> endobj -641 0 obj +647 0 obj << /Type /Annot /Subtype /Link @@ -9621,7 +9799,7 @@ endobj >> >> endobj -642 0 obj +648 0 obj << /Type /Annot /Subtype /Link @@ -9636,7 +9814,7 @@ endobj >> >> endobj -643 0 obj +649 0 obj << /Type /Annot /Subtype /Link @@ -9651,7 +9829,7 @@ endobj >> >> endobj -644 0 obj +650 0 obj << /Type /Annot /Subtype /Link @@ -9666,7 +9844,7 @@ endobj >> >> endobj -645 0 obj +651 0 obj << /Type /Annot /Subtype /Link @@ -9681,7 +9859,7 @@ endobj >> >> endobj -646 0 obj +652 0 obj << /Type /Annot /Subtype /Link @@ -9696,7 +9874,7 @@ endobj >> >> endobj -647 0 obj +653 0 obj << /Filter /FlateDecode /Length 1124 @@ -9708,19 +9886,19 @@ lξ ʚw( Ն\V}z%M4lX /0qݬǫ[uWkCm}߿^OV=R']Q5O]~˱1wBٺ!{viY|k&$:V@H#xEOYn(g9,~ {C:'pzӃ~ .TP~x.; 6pWM?_|o endstream endobj -648 0 obj +654 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 647 0 R +/Contents 653 0 R /Resources 4 0 R -/Annots [ 649 0 R 650 0 R 651 0 R 652 0 R 653 0 R 654 0 R 655 0 R 656 0 R 657 0 R 658 0 R 659 0 R 660 0 R ] +/Annots [ 655 0 R 656 0 R 657 0 R 658 0 R 659 0 R 660 0 R 661 0 R 662 0 R 663 0 R 664 0 R 665 0 R 666 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -649 0 obj +655 0 obj << /Type /Annot /Subtype /Link @@ -9735,7 +9913,7 @@ endobj >> >> endobj -650 0 obj +656 0 obj << /Type /Annot /Subtype /Link @@ -9750,7 +9928,7 @@ endobj >> >> endobj -651 0 obj +657 0 obj << /Type /Annot /Subtype /Link @@ -9765,7 +9943,7 @@ endobj >> >> endobj -652 0 obj +658 0 obj << /Type /Annot /Subtype /Link @@ -9780,7 +9958,7 @@ endobj >> >> endobj -653 0 obj +659 0 obj << /Type /Annot /Subtype /Link @@ -9795,7 +9973,7 @@ endobj >> >> endobj -654 0 obj +660 0 obj << /Type /Annot /Subtype /Link @@ -9810,7 +9988,7 @@ endobj >> >> endobj -655 0 obj +661 0 obj << /Type /Annot /Subtype /Link @@ -9825,7 +10003,7 @@ endobj >> >> endobj -656 0 obj +662 0 obj << /Type /Annot /Subtype /Link @@ -9840,7 +10018,7 @@ endobj >> >> endobj -657 0 obj +663 0 obj << /Type /Annot /Subtype /Link @@ -9855,7 +10033,7 @@ endobj >> >> endobj -658 0 obj +664 0 obj << /Type /Annot /Subtype /Link @@ -9870,7 +10048,7 @@ endobj >> >> endobj -659 0 obj +665 0 obj << /Type /Annot /Subtype /Link @@ -9885,7 +10063,7 @@ endobj >> >> endobj -660 0 obj +666 0 obj << /Type /Annot /Subtype /Link @@ -9900,7 +10078,7 @@ endobj >> >> endobj -661 0 obj +667 0 obj << /Filter /FlateDecode /Length 1760 @@ -9919,19 +10097,19 @@ Kl $n 4A٧=lf J{hTS[‹3$ PJ1ͦ"'-[$f*0/!\,8sdaZ"T<ݷ`UI:O-τp7s endstream endobj -662 0 obj +668 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 661 0 R +/Contents 667 0 R /Resources 4 0 R -/Annots [ 663 0 R 664 0 R 665 0 R 666 0 R 667 0 R 668 0 R 669 0 R 670 0 R ] +/Annots [ 669 0 R 670 0 R 671 0 R 672 0 R 673 0 R 674 0 R 675 0 R 676 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -663 0 obj +669 0 obj << /Type /Annot /Subtype /Link @@ -9946,7 +10124,7 @@ endobj >> >> endobj -664 0 obj +670 0 obj << /Type /Annot /Subtype /Link @@ -9961,7 +10139,7 @@ endobj >> >> endobj -665 0 obj +671 0 obj << /Type /Annot /Subtype /Link @@ -9976,7 +10154,7 @@ endobj >> >> endobj -666 0 obj +672 0 obj << /Type /Annot /Subtype /Link @@ -9991,7 +10169,7 @@ endobj >> >> endobj -667 0 obj +673 0 obj << /Type /Annot /Subtype /Link @@ -10006,7 +10184,7 @@ endobj >> >> endobj -668 0 obj +674 0 obj << /Type /Annot /Subtype /Link @@ -10021,7 +10199,7 @@ endobj >> >> endobj -669 0 obj +675 0 obj << /Type /Annot /Subtype /Link @@ -10036,7 +10214,7 @@ endobj >> >> endobj -670 0 obj +676 0 obj << /Type /Annot /Subtype /Link @@ -10051,7 +10229,7 @@ endobj >> >> endobj -671 0 obj +677 0 obj << /Filter /FlateDecode /Length 2775 @@ -10074,18 +10252,18 @@ Y ñ,<&s&HNuҲa.lJ>r)~q8qXyQ#0grθ gI6O돃e5X75k &$bx  Ao5/x%o8g"2^T<1cZDr_S\pAHbqBieN ś'Q_=WN 9)*: 3%r=dv`s¡o>av-@ U/eRL;P%KJR-.Prz]},9lZwsQxwv`֚ endstream endobj -672 0 obj +678 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 671 0 R +/Contents 677 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -673 0 obj +679 0 obj << /Filter /FlateDecode /Length 2222 @@ -10106,19 +10284,19 @@ r B<1~9q7-Zݓg(sTD:ttUGkbW_f3-#qmHGZ(e7&7nؤf\p){nVg2- $iߨ8(P8vBet"!Siv6\*k`]l)#9l daLiZ7KgfFz F8BK>~/5dЭȺڷ-\_g{ endstream endobj -674 0 obj +680 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 673 0 R +/Contents 679 0 R /Resources 4 0 R -/Annots [ 675 0 R 676 0 R 677 0 R 678 0 R 679 0 R 680 0 R 681 0 R ] +/Annots [ 681 0 R 682 0 R 683 0 R 684 0 R 685 0 R 686 0 R 687 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -675 0 obj +681 0 obj << /Type /Annot /Subtype /Link @@ -10133,7 +10311,7 @@ endobj >> >> endobj -676 0 obj +682 0 obj << /Type /Annot /Subtype /Link @@ -10148,7 +10326,7 @@ endobj >> >> endobj -677 0 obj +683 0 obj << /Type /Annot /Subtype /Link @@ -10163,7 +10341,7 @@ endobj >> >> endobj -678 0 obj +684 0 obj << /Type /Annot /Subtype /Link @@ -10178,7 +10356,7 @@ endobj >> >> endobj -679 0 obj +685 0 obj << /Type /Annot /Subtype /Link @@ -10193,7 +10371,7 @@ endobj >> >> endobj -680 0 obj +686 0 obj << /Type /Annot /Subtype /Link @@ -10208,7 +10386,7 @@ endobj >> >> endobj -681 0 obj +687 0 obj << /Type /Annot /Subtype /Link @@ -10223,7 +10401,7 @@ endobj >> >> endobj -682 0 obj +688 0 obj << /Filter /FlateDecode /Length 1507 @@ -10236,19 +10414,19 @@ G.\ &L.)T ͽԍtLllmc7v 7X+..7R޵ ς}> endobj -684 0 obj +690 0 obj << /Type /Annot /Subtype /Link @@ -10263,7 +10441,7 @@ endobj >> >> endobj -685 0 obj +691 0 obj << /Type /Annot /Subtype /Link @@ -10278,7 +10456,7 @@ endobj >> >> endobj -686 0 obj +692 0 obj << /Type /Annot /Subtype /Link @@ -10293,7 +10471,7 @@ endobj >> >> endobj -687 0 obj +693 0 obj << /Filter /FlateDecode /Length 1051 @@ -10309,19 +10487,19 @@ P O| XXRYr:Oρ W N|Tdlx@z& I=TEH 7AxËOM 0H8#?i[dkszsEX9"}ArR4E O$TZkkʯ+A9kX*~Ga9S#voLJKkj a%lRڅGF}p+zC^禌g#;- LzVNJ|kbW*Zs[l"ʧ*]|0f endstream endobj -688 0 obj +694 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 687 0 R +/Contents 693 0 R /Resources 4 0 R -/Annots [ 689 0 R 690 0 R 691 0 R ] +/Annots [ 695 0 R 696 0 R 697 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -689 0 obj +695 0 obj << /Type /Annot /Subtype /Link @@ -10336,7 +10514,7 @@ endobj >> >> endobj -690 0 obj +696 0 obj << /Type /Annot /Subtype /Link @@ -10351,7 +10529,7 @@ endobj >> >> endobj -691 0 obj +697 0 obj << /Type /Annot /Subtype /Link @@ -10366,7 +10544,7 @@ endobj >> >> endobj -692 0 obj +698 0 obj << /Filter /FlateDecode /Length 1331 @@ -10381,19 +10559,19 @@ x VSø</z:B{:\56Bj 7 W>m7z]Ly7V lz,,噂*ǹ90V%"Auy2ޯdFU4A'6s/a) pǐ3D endstream endobj -693 0 obj +699 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 692 0 R +/Contents 698 0 R /Resources 4 0 R -/Annots [ 694 0 R 695 0 R ] +/Annots [ 700 0 R 701 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -694 0 obj +700 0 obj << /Type /Annot /Subtype /Link @@ -10408,7 +10586,7 @@ endobj >> >> endobj -695 0 obj +701 0 obj << /Type /Annot /Subtype /Link @@ -10423,7 +10601,7 @@ endobj >> >> endobj -696 0 obj +702 0 obj << /Filter /FlateDecode /Length 2206 @@ -10440,19 +10618,19 @@ eM z䓲ju@brG3Ⱄ-t蝟|rbv?iƼ4l]>Wn~sYJkxa 'rsYYR4,j]Eq^LCr5->@,okeg"}Vx8]w!<L Zu49+JC6jnN :A jt] y)gH(B.eKdt 0!ǝJgDLlwR$KE3Ҫ ;.u&u7yL,NʖA2MlUTBd y_ Es/=>G'#clrPXGQwDIGm8^qmikm_L'tEѳPnLJӮnࠎoux"N SJ1SҭHޓGgYL*?V ^C9s8MV?Ϲjn0"9Kj{ZנJg9WFq?HeH\/d-Уڗc8y%SE$-JW?p>*bA*jЌ,UsPM߿/XɬI GƗxDz+<ޔ!)+aAm+0r7xdF|),/7_Mtsф)tkwcΜ(:x!0l|$}NU-eJD:3EP OjA:=%Άp>,AT,]CvBknzZ{ 6 endstream endobj -697 0 obj +703 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 696 0 R +/Contents 702 0 R /Resources 4 0 R -/Annots [ 698 0 R 699 0 R 700 0 R ] +/Annots [ 704 0 R 705 0 R 706 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -698 0 obj +704 0 obj << /Type /Annot /Subtype /Link @@ -10467,7 +10645,7 @@ endobj >> >> endobj -699 0 obj +705 0 obj << /Type /Annot /Subtype /Link @@ -10482,7 +10660,7 @@ endobj >> >> endobj -700 0 obj +706 0 obj << /Type /Annot /Subtype /Link @@ -10497,7 +10675,7 @@ endobj >> >> endobj -701 0 obj +707 0 obj << /Filter /FlateDecode /Length 1601 @@ -10514,19 +10692,19 @@ GJ Cb~p"C/$א`…"]Nİ6ُ[g@J~R endstream endobj -702 0 obj +708 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 701 0 R +/Contents 707 0 R /Resources 4 0 R -/Annots [ 703 0 R 704 0 R 705 0 R 706 0 R 707 0 R 708 0 R 709 0 R 710 0 R 711 0 R 712 0 R ] +/Annots [ 709 0 R 710 0 R 711 0 R 712 0 R 713 0 R 714 0 R 715 0 R 716 0 R 717 0 R 718 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -703 0 obj +709 0 obj << /Type /Annot /Subtype /Link @@ -10541,7 +10719,7 @@ endobj >> >> endobj -704 0 obj +710 0 obj << /Type /Annot /Subtype /Link @@ -10556,7 +10734,7 @@ endobj >> >> endobj -705 0 obj +711 0 obj << /Type /Annot /Subtype /Link @@ -10571,7 +10749,7 @@ endobj >> >> endobj -706 0 obj +712 0 obj << /Type /Annot /Subtype /Link @@ -10586,7 +10764,7 @@ endobj >> >> endobj -707 0 obj +713 0 obj << /Type /Annot /Subtype /Link @@ -10601,7 +10779,7 @@ endobj >> >> endobj -708 0 obj +714 0 obj << /Type /Annot /Subtype /Link @@ -10616,7 +10794,7 @@ endobj >> >> endobj -709 0 obj +715 0 obj << /Type /Annot /Subtype /Link @@ -10631,7 +10809,7 @@ endobj >> >> endobj -710 0 obj +716 0 obj << /Type /Annot /Subtype /Link @@ -10646,7 +10824,7 @@ endobj >> >> endobj -711 0 obj +717 0 obj << /Type /Annot /Subtype /Link @@ -10661,7 +10839,7 @@ endobj >> >> endobj -712 0 obj +718 0 obj << /Type /Annot /Subtype /Link @@ -10676,7 +10854,7 @@ endobj >> >> endobj -713 0 obj +719 0 obj << /Filter /FlateDecode /Length 1978 @@ -10693,19 +10871,19 @@ a Q({~.N@4tΕwHxKe endstream endobj -714 0 obj +720 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 713 0 R +/Contents 719 0 R /Resources 4 0 R -/Annots [ 715 0 R 716 0 R ] +/Annots [ 721 0 R 722 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -715 0 obj +721 0 obj << /Type /Annot /Subtype /Link @@ -10720,7 +10898,7 @@ endobj >> >> endobj -716 0 obj +722 0 obj << /Type /Annot /Subtype /Link @@ -10735,7 +10913,7 @@ endobj >> >> endobj -717 0 obj +723 0 obj << /Filter /FlateDecode /Length 3337 @@ -10757,18 +10935,18 @@ f fB3s endstream endobj -718 0 obj +724 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 717 0 R +/Contents 723 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -719 0 obj +725 0 obj << /Filter /FlateDecode /Length 3096 @@ -10789,18 +10967,18 @@ v j_>C> endobj -721 0 obj +727 0 obj << /Filter /FlateDecode /Length 2138 @@ -10817,19 +10995,19 @@ yN Fyi J)CMQ׋c(v@5|eމ!7(iN/@z10z-dXcv_M%`5ȳl_ ?`+9Hzj;ĥb;. &z%ެ̸W %TPΩR^9-[]V|z3U8OFMhNs$αe;)iw?Y endstream endobj -722 0 obj +728 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 721 0 R +/Contents 727 0 R /Resources 4 0 R -/Annots [ 723 0 R ] +/Annots [ 729 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -723 0 obj +729 0 obj << /Type /Annot /Subtype /Link @@ -10844,7 +11022,7 @@ endobj >> >> endobj -724 0 obj +730 0 obj << /Filter /FlateDecode /Length 367 @@ -10854,19 +11032,19 @@ x \\z 7? Sc c [xӯx eR ZFTϨh@%p2iM?AilG8n98$YmQ.nd;B[-uz0ke8 endstream endobj -725 0 obj +731 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 724 0 R +/Contents 730 0 R /Resources 4 0 R -/Annots [ 726 0 R ] +/Annots [ 732 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -726 0 obj +732 0 obj << /Type /Annot /Subtype /Link @@ -10881,7 +11059,7 @@ endobj >> >> endobj -727 0 obj +733 0 obj << /Filter /FlateDecode /Length 612 @@ -10891,19 +11069,19 @@ x qÉȼ=u p eOx6ŷ+C?l4Z[626~0_ES롭_8?5~[?AnzNo'Bb+D2%B#7ρJ `MNhs犺+hCxll"jYדJɷiRߝ}.z\p(Ţ ULsk+ɂ~j#7ґ^:qh[ MDNJШZbZ=PhRZPSB\385~:(>s6NrQզqndF8W'c endstream endobj -728 0 obj +734 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 727 0 R +/Contents 733 0 R /Resources 4 0 R -/Annots [ 729 0 R 730 0 R ] +/Annots [ 735 0 R 736 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -729 0 obj +735 0 obj << /Type /Annot /Subtype /Link @@ -10918,7 +11096,7 @@ endobj >> >> endobj -730 0 obj +736 0 obj << /Type /Annot /Subtype /Link @@ -10933,7 +11111,7 @@ endobj >> >> endobj -731 0 obj +737 0 obj << /Filter /FlateDecode /Length 365 @@ -10942,19 +11120,19 @@ stream xN0 y·K8 .HE !&6_I;uowɣ ٫$RT89h89{MJ^h1BN1}ܫE#D!@&[w$@}w 4[Pw(3R&tcrNͧϡdثL}*?} [>خѲ~tٞu#U1Rb%韃 ~bk۸"9N+|J<|3b}?n.PFZK-U8 kOPɀ(\u@ ԟbV)`9jno!CUCK-lЮi2|` endstream endobj -732 0 obj +738 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 731 0 R +/Contents 737 0 R /Resources 4 0 R -/Annots [ 733 0 R ] +/Annots [ 739 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -733 0 obj +739 0 obj << /Type /Annot /Subtype /Link @@ -10969,7 +11147,7 @@ endobj >> >> endobj -734 0 obj +740 0 obj << /Filter /FlateDecode /Length 3597 @@ -10996,19 +11174,19 @@ L H0a1u֡_E{z=&VX endstream endobj -735 0 obj +741 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 734 0 R +/Contents 740 0 R /Resources 4 0 R -/Annots [ 736 0 R ] +/Annots [ 742 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -736 0 obj +742 0 obj << /Type /Annot /Subtype /Link @@ -11023,7 +11201,7 @@ endobj >> >> endobj -737 0 obj +743 0 obj << /Filter /FlateDecode /Length 3905 @@ -11046,18 +11224,18 @@ B ƺ(Ss0Wԋ΅^S;Jh[G>;nkY!nmoOCJԲ੍ [K EyFOW .'0 t|7\/zBk@B:ɯ -×N|> endobj -739 0 obj +745 0 obj << /Filter /FlateDecode /Length 3459 @@ -11076,18 +11254,18 @@ y O|1Ѓ`|-. 0QPC E͊ endstream endobj -740 0 obj +746 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 739 0 R +/Contents 745 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -741 0 obj +747 0 obj << /Filter /FlateDecode /Length 3360 @@ -11104,19 +11282,19 @@ x y^2.<6K=GjpJMmh|y\'H{n{Y*UZx'VTg2-DZRv0ʺ(kL[.Fooݴ$ pf"۱qI73zA$MTC4$D,+ɶ^(ߍu$`4e!= f/:U%ӽtyPRCa6 endstream endobj -742 0 obj +748 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 741 0 R +/Contents 747 0 R /Resources 4 0 R -/Annots [ 743 0 R ] +/Annots [ 749 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -743 0 obj +749 0 obj << /Type /Annot /Subtype /Link @@ -11131,7 +11309,7 @@ endobj >> >> endobj -744 0 obj +750 0 obj << /Filter /FlateDecode /Length 1496 @@ -11146,19 +11324,19 @@ zqh}f^ EfC1QyH@*o=iQJSj@;KصD*28N cn~[] T$UqJFAl⇗eZt}.cD:O}Y9ϳϿI+cjuһ&CY7%$&&kE. zŦ}W:_|]4UۨtCǖ A~R0󧝲}Ʉ̉Phu?>¹c@:Dۯ${TdM+D|t  M0*w䋃3S ߃AMA?mV ʘsn͟e#Lѱ nzbf(їqQG endstream endobj -745 0 obj +751 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 744 0 R +/Contents 750 0 R /Resources 4 0 R -/Annots [ 746 0 R 747 0 R 748 0 R 749 0 R 750 0 R 751 0 R ] +/Annots [ 752 0 R 753 0 R 754 0 R 755 0 R 756 0 R 757 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -746 0 obj +752 0 obj << /Type /Annot /Subtype /Link @@ -11173,7 +11351,7 @@ endobj >> >> endobj -747 0 obj +753 0 obj << /Type /Annot /Subtype /Link @@ -11188,7 +11366,7 @@ endobj >> >> endobj -748 0 obj +754 0 obj << /Type /Annot /Subtype /Link @@ -11203,7 +11381,7 @@ endobj >> >> endobj -749 0 obj +755 0 obj << /Type /Annot /Subtype /Link @@ -11218,7 +11396,7 @@ endobj >> >> endobj -750 0 obj +756 0 obj << /Type /Annot /Subtype /Link @@ -11233,7 +11411,7 @@ endobj >> >> endobj -751 0 obj +757 0 obj << /Type /Annot /Subtype /Link @@ -11248,7 +11426,7 @@ endobj >> >> endobj -752 0 obj +758 0 obj << /Filter /FlateDecode /Length 2184 @@ -11263,19 +11441,19 @@ T v(K endstream endobj -753 0 obj +759 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 752 0 R +/Contents 758 0 R /Resources 4 0 R -/Annots [ 754 0 R 755 0 R 756 0 R 757 0 R 758 0 R 759 0 R 760 0 R 761 0 R ] +/Annots [ 760 0 R 761 0 R 762 0 R 763 0 R 764 0 R 765 0 R 766 0 R 767 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -754 0 obj +760 0 obj << /Type /Annot /Subtype /Link @@ -11290,7 +11468,7 @@ endobj >> >> endobj -755 0 obj +761 0 obj << /Type /Annot /Subtype /Link @@ -11305,7 +11483,7 @@ endobj >> >> endobj -756 0 obj +762 0 obj << /Type /Annot /Subtype /Link @@ -11320,7 +11498,7 @@ endobj >> >> endobj -757 0 obj +763 0 obj << /Type /Annot /Subtype /Link @@ -11335,7 +11513,7 @@ endobj >> >> endobj -758 0 obj +764 0 obj << /Type /Annot /Subtype /Link @@ -11350,7 +11528,7 @@ endobj >> >> endobj -759 0 obj +765 0 obj << /Type /Annot /Subtype /Link @@ -11365,7 +11543,7 @@ endobj >> >> endobj -760 0 obj +766 0 obj << /Type /Annot /Subtype /Link @@ -11380,7 +11558,7 @@ endobj >> >> endobj -761 0 obj +767 0 obj << /Type /Annot /Subtype /Link @@ -11395,7 +11573,7 @@ endobj >> >> endobj -762 0 obj +768 0 obj << /Filter /FlateDecode /Length 2978 @@ -11414,19 +11592,19 @@ H ? Ϋ14"dY}} 9&/;Pjb'th6¡'?mgjG!˧%ͩ6(&y"h?I-)?7иꚁJa*B+!AļUrɖ#쎓vFfpB!F=YRձ+:VVfg9[̼ڞG~ޣ h,+hhNÄZ<=Boi,A-hSWtY"46C3h|GOGZ]HMFlghJuh=Ww!w48PGkl?efPd wYUˮPJk+kaeM+X߀IK[ݶļ!N>SWCO_}'<:֍-uޖ#|U *, endstream endobj -763 0 obj +769 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 762 0 R +/Contents 768 0 R /Resources 4 0 R -/Annots [ 764 0 R ] +/Annots [ 770 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -764 0 obj +770 0 obj << /Type /Annot /Subtype /Link @@ -11441,7 +11619,7 @@ endobj >> >> endobj -765 0 obj +771 0 obj << /Filter /FlateDecode /Length 1952 @@ -11455,19 +11633,19 @@ x AA N9ʛ2[o3Fy}Si9abySsy"g/԰$wc=7,ρIKS14yCz'›kx-g!Sr$~MlBV?1yN"R(McZf_y㴑?Ƴ2:+s'{0B3u+3s>/ac`+{iW꒎I{v+< endstream endobj -766 0 obj +772 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 765 0 R +/Contents 771 0 R /Resources 4 0 R -/Annots [ 767 0 R 768 0 R 769 0 R ] +/Annots [ 773 0 R 774 0 R 775 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -767 0 obj +773 0 obj << /Type /Annot /Subtype /Link @@ -11482,7 +11660,7 @@ endobj >> >> endobj -768 0 obj +774 0 obj << /Type /Annot /Subtype /Link @@ -11497,7 +11675,7 @@ endobj >> >> endobj -769 0 obj +775 0 obj << /Type /Annot /Subtype /Link @@ -11512,7 +11690,7 @@ endobj >> >> endobj -770 0 obj +776 0 obj << /Filter /FlateDecode /Length 457 @@ -11521,19 +11699,19 @@ stream xSn0+޵+UCR)$!yTI m9!<^Ό'QcnEp?PLh=K߾ŸĢyNVkZz0`Q$f! _7hDV-03{Ephn8R9,(գ'EKh!L%(wv9zR1VBJ,22*Տz`rܰ@qk&qbH޿}MHi9}!EiDW5EƅH2unRS̄;AgGT֐)*ܒӑKk;FuF!K ڊ3I cr#fJ5z{jIO.fF70i{ m9!|QGuV Z'@PKb'U-[;nJ`ߩ endstream endobj -771 0 obj +777 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 770 0 R +/Contents 776 0 R /Resources 4 0 R -/Annots [ 772 0 R 773 0 R 774 0 R ] +/Annots [ 778 0 R 779 0 R 780 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -772 0 obj +778 0 obj << /Type /Annot /Subtype /Link @@ -11548,7 +11726,7 @@ endobj >> >> endobj -773 0 obj +779 0 obj << /Type /Annot /Subtype /Link @@ -11563,7 +11741,7 @@ endobj >> >> endobj -774 0 obj +780 0 obj << /Type /Annot /Subtype /Link @@ -11578,7 +11756,7 @@ endobj >> >> endobj -775 0 obj +781 0 obj << /Filter /FlateDecode /Length 1132 @@ -11591,19 +11769,19 @@ x m[)^:m5_@sH6 R N+$x黱2|mzTT6+~>Cx/W*7o B!!|K@"뵛^;FKDr7eD$k!su/wEOٷG+(ݤp0>@b[<=<<[gsJyDxB+>UZAL(W-5ˌ8}nt:yV1*b9gb<+<}`6GP>!#Io`H%9K ˹LflUX3`B ?={ endstream endobj -776 0 obj +782 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 775 0 R +/Contents 781 0 R /Resources 4 0 R -/Annots [ 777 0 R 778 0 R ] +/Annots [ 783 0 R 784 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -777 0 obj +783 0 obj << /Type /Annot /Subtype /Link @@ -11618,7 +11796,7 @@ endobj >> >> endobj -778 0 obj +784 0 obj << /Type /Annot /Subtype /Link @@ -11633,7 +11811,7 @@ endobj >> >> endobj -779 0 obj +785 0 obj << /Filter /FlateDecode /Length 3314 @@ -11651,18 +11829,18 @@ uY ТbR63*k2}.z0ŧ2d $) \}\r'瞈숖 endstream endobj -780 0 obj +786 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 779 0 R +/Contents 785 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -781 0 obj +787 0 obj << /Filter /FlateDecode /Length 3792 @@ -11684,18 +11862,18 @@ FXPu UƮ R;̘Zq[kZ/hx2YJ06V. K5uɀES>_aM^ I7<ҞjlOkoI!5QP2_TNbkF&)X _B@Ŷϼ'U]3cΕtH;fJG()vآ?<ȏ:^ Q+m*3<޹-).<]5xk5oKv]v)j.flnBbm }@0O/kU)NS%fapD`:7M Y 3S`M;mP#| EɋI/5HmJC 69xXJ2Eo`̻MKacJQl[9^6#4#xw.s]x0~FV4\ `>M%|erZ\z;HteZԎ3:DŽ[kbtͭac Kw]s]7LX4| 7|k9*Uo p*_5:1&o#Í4<5[ݪ1Mb:-xUO_@~3#Dw+ rISpn$$F?u#T!sRD + SPE%vX.h""DFL4ՅFddN}5$0 1#aË%Ȭi˱ZOIiOb endstream endobj -782 0 obj +788 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 781 0 R +/Contents 787 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -783 0 obj +789 0 obj << /Filter /FlateDecode /Length 3994 @@ -11723,18 +11901,18 @@ cZ 6su"?]&&K8׈9'>eb0+/AOC\bEssІHAJ endstream endobj -784 0 obj +790 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 783 0 R +/Contents 789 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -785 0 obj +791 0 obj << /Filter /FlateDecode /Length 1228 @@ -11750,19 +11928,19 @@ g} b;)XpzynYP0׆Qy1hx+ endstream endobj -786 0 obj +792 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 785 0 R +/Contents 791 0 R /Resources 4 0 R -/Annots [ 787 0 R 788 0 R 789 0 R 790 0 R ] +/Annots [ 793 0 R 794 0 R 795 0 R 796 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -787 0 obj +793 0 obj << /Type /Annot /Subtype /Link @@ -11777,7 +11955,7 @@ endobj >> >> endobj -788 0 obj +794 0 obj << /Type /Annot /Subtype /Link @@ -11792,7 +11970,7 @@ endobj >> >> endobj -789 0 obj +795 0 obj << /Type /Annot /Subtype /Link @@ -11807,7 +11985,7 @@ endobj >> >> endobj -790 0 obj +796 0 obj << /Type /Annot /Subtype /Link @@ -11822,7 +12000,7 @@ endobj >> >> endobj -791 0 obj +797 0 obj << /Filter /FlateDecode /Length 1125 @@ -11832,19 +12010,19 @@ x T"T%I#";gnUF`L)kP/ )V@9MGAK"\K`F\- sě~K5nGK^΋H]i9E_獭SfP컱5cht4èC(z3|-4@ }eSl .:.K#Ϗ]I{e*v6> endobj -793 0 obj +799 0 obj << /Type /Annot /Subtype /Link @@ -11859,7 +12037,7 @@ endobj >> >> endobj -794 0 obj +800 0 obj << /Type /Annot /Subtype /Link @@ -11874,7 +12052,7 @@ endobj >> >> endobj -795 0 obj +801 0 obj << /Type /Annot /Subtype /Link @@ -11889,7 +12067,7 @@ endobj >> >> endobj -796 0 obj +802 0 obj << /Filter /FlateDecode /Length 479 @@ -11899,19 +12077,19 @@ x jyю`vS+cE5g| endstream endobj -797 0 obj +803 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 796 0 R +/Contents 802 0 R /Resources 4 0 R -/Annots [ 798 0 R 799 0 R ] +/Annots [ 804 0 R 805 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -798 0 obj +804 0 obj << /Type /Annot /Subtype /Link @@ -11926,7 +12104,7 @@ endobj >> >> endobj -799 0 obj +805 0 obj << /Type /Annot /Subtype /Link @@ -11941,7 +12119,7 @@ endobj >> >> endobj -800 0 obj +806 0 obj << /Filter /FlateDecode /Length 2151 @@ -11959,19 +12137,19 @@ O fV,nh;#R,\nұ7b7Fj endstream endobj -801 0 obj +807 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 800 0 R +/Contents 806 0 R /Resources 4 0 R -/Annots [ 802 0 R 803 0 R 804 0 R ] +/Annots [ 808 0 R 809 0 R 810 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -802 0 obj +808 0 obj << /Type /Annot /Subtype /Link @@ -11986,7 +12164,7 @@ endobj >> >> endobj -803 0 obj +809 0 obj << /Type /Annot /Subtype /Link @@ -12001,7 +12179,7 @@ endobj >> >> endobj -804 0 obj +810 0 obj << /Type /Annot /Subtype /Link @@ -12016,7 +12194,7 @@ endobj >> >> endobj -805 0 obj +811 0 obj << /Filter /FlateDecode /Length 2554 @@ -12031,19 +12209,19 @@ j ; oLHGmXȭXj)' u>>2H]ztRCPS#.4M9[*4 U^4,N**vo8^ Qz\<â%guI5Ǧ s%t#;:BbsÊuL+3EwƟ`A }7> endstream endobj -806 0 obj +812 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 805 0 R +/Contents 811 0 R /Resources 4 0 R -/Annots [ 807 0 R 808 0 R 809 0 R ] +/Annots [ 813 0 R 814 0 R 815 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -807 0 obj +813 0 obj << /Type /Annot /Subtype /Link @@ -12058,7 +12236,7 @@ endobj >> >> endobj -808 0 obj +814 0 obj << /Type /Annot /Subtype /Link @@ -12073,7 +12251,7 @@ endobj >> >> endobj -809 0 obj +815 0 obj << /Type /Annot /Subtype /Link @@ -12088,7 +12266,7 @@ endobj >> >> endobj -810 0 obj +816 0 obj << /Filter /FlateDecode /Length 1177 @@ -12098,19 +12276,19 @@ x ƈ1*0)D+̘?/'J-G/z#dA40b9mS\SѨ ZȸSPSB؉ŤjKt y=iHM4C޺+h֍90ƏO?k{|vt*aTOY_-t0ZZ2뙾bzHOh:9\=VLYltcƇ׻:bl\+f+X-4XT 60m mʨG;?f !4y}]<#bc)*{bysF 6|m0`O {(Wy^a=irGXRzݚ6J8]P$YhP"־8ϟ=>5 ¾Εy)<^-UϾP驗Y7p$BnpF/J(^б C#CX C]C텤iF)ڲuOjkJp%N-arc$S8R_&>@T`g:҈.%u$) 8xD~4RGKRo@mG$_{${9[8zlqı^XKڱ(T4hD6{NƐ{D,!3pZ,[p&}ɤ\6Pw弃s,VZ}m|{͌e뛗 C~h.Kovbņ52 vmgݎwxHKUY-fQ4L﬍[t7FYz6E>k8_=4Uv;WꣲZyK)-QF˟^rǑ/0͎SS'2+7NgJ\)cT)D&5U]&uL.gR0$P ,\Χñ)gvzH4[uxjޘ+#[ox?]# endstream endobj -811 0 obj +817 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 810 0 R +/Contents 816 0 R /Resources 4 0 R -/Annots [ 812 0 R ] +/Annots [ 818 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -812 0 obj +818 0 obj << /Type /Annot /Subtype /Link @@ -12125,7 +12303,7 @@ endobj >> >> endobj -813 0 obj +819 0 obj << /Filter /FlateDecode /Length 381 @@ -12135,19 +12313,19 @@ x A !ePж/%; qf8{Ñ` 'f*U~ ŒMc$-{b\,%U),>cyk%ES^{nOŇ簠Щ/Z;۩sbk|.6ǿd6+h{VgQ> endobj -815 0 obj +821 0 obj << /Type /Annot /Subtype /Link @@ -12162,7 +12340,7 @@ endobj >> >> endobj -816 0 obj +822 0 obj << /Filter /FlateDecode /Length 695 @@ -12172,19 +12350,19 @@ x N7}^]{w(Lk*,Q:;h49¾3&Tc*eˣ Xqđ:Y9Xd}uzWxksw,wr]S$v>RV!CE5e2 endstream endobj -817 0 obj +823 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 816 0 R +/Contents 822 0 R /Resources 4 0 R -/Annots [ 818 0 R 819 0 R ] +/Annots [ 824 0 R 825 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -818 0 obj +824 0 obj << /Type /Annot /Subtype /Link @@ -12199,7 +12377,7 @@ endobj >> >> endobj -819 0 obj +825 0 obj << /Type /Annot /Subtype /Link @@ -12214,7 +12392,7 @@ endobj >> >> endobj -820 0 obj +826 0 obj << /Filter /FlateDecode /Length 382 @@ -12226,19 +12404,19 @@ S FW ]CzNLZ폱$-x[;> endobj -822 0 obj +828 0 obj << /Type /Annot /Subtype /Link @@ -12253,7 +12431,7 @@ endobj >> >> endobj -823 0 obj +829 0 obj << /Filter /FlateDecode /Length 555 @@ -12265,19 +12443,19 @@ t lG= endstream endobj -824 0 obj +830 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 823 0 R +/Contents 829 0 R /Resources 4 0 R -/Annots [ 825 0 R 826 0 R ] +/Annots [ 831 0 R 832 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -825 0 obj +831 0 obj << /Type /Annot /Subtype /Link @@ -12292,84 +12470,11 @@ endobj >> >> endobj -826 0 obj -<< -/Type /Annot -/Subtype /Link -/Rect [ 37.466457 690.402822 37.466457 677.999622 ] -/BS << -/W 0 ->> -/A << -/Type /Action -/S /URI -/URI (./Images/Figma/PosterV5.png) ->> ->> -endobj -827 0 obj -<< -/Filter /FlateDecode -/Length 383 ->> -stream -xRMo1 Wډ;RTzh{AC,7ıHd*ެj?c`b3C!Y+jlJ.ޢ cT4g>o[dER"z>ǧaFS_Z-iwSRk9 8YE>VW>8_}J:$uɹa' x/-޼ g92 J\Ϻڷӂ-&% >?~qkfP*t^e43®\N+>V]d9u%Nw/Uj?:N֋:a? .@/iom9B57d-xk~Vw <'@ȝ?@K w6f8W]q5 -endstream -endobj -828 0 obj -<< -/Type /Page -/Parent 1 0 R -/MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 827 0 R -/Resources 4 0 R -/Annots [ 829 0 R ] -/TrimBox [ 0 0 595.275591 841.889764 ] -/BleedBox [ 0 0 595.275591 841.889764 ] ->> -endobj -829 0 obj -<< -/Type /Annot -/Subtype /Link -/Rect [ 37.466457 771.023622 557.809134 35.138876 ] -/BS << -/W 0 ->> -/A << -/Type /Action -/S /URI -/URI (./Images/Figma/PosterV5.png) ->> ->> -endobj -830 0 obj -<< -/Filter /FlateDecode -/Length 664 ->> -stream -xUMo0 WL%IECK 0MO#%ʖ2̎cY( - ZS0u#U-6ugKt9l B*&#YDp;1xټ=Mߦ_ )DlĒ(ߞ-Ӆ5Wn1T Fw ;_1tO﹗;p9^x?@Ĭ(1d(7j:xal[4+HbCN`c 3؀W`@@p8v8Ug뜐׾Ԯ_vR#^~`I}x־Y+fU.vߠ8W_9~ªAGT|!~[xu>]F* kѹ)6#A{w{Z)ߓ-15iDI*jH B>Xcru%HTA?sfIlg[d,Æhi❔ǰ!:ޘu[6@zj7&p⺘aGy&>x;^uip XR gpɎn,}+\aj9z-.[5=l 2a,|r<ՙ,!] Orh0A}Ƣ?L% %r16H=t:Hig]sT;9߮N2U6<a= -endstream -endobj -831 0 obj -<< -/Type /Page -/Parent 1 0 R -/MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 830 0 R -/Resources 4 0 R -/Annots [ 832 0 R 833 0 R ] -/TrimBox [ 0 0 595.275591 841.889764 ] -/BleedBox [ 0 0 595.275591 841.889764 ] ->> -endobj 832 0 obj << /Type /Annot /Subtype /Link -/Rect [ 297.637795 771.023622 297.637795 758.620422 ] +/Rect [ 37.466457 690.402822 37.466457 677.999622 ] /BS << /W 0 >> @@ -12382,6 +12487,79 @@ endobj endobj 833 0 obj << +/Filter /FlateDecode +/Length 383 +>> +stream +xRMo1 Wډ;RTzh{AC,7ıHd*ެj?c`b3C!Y+jlJ.ޢ cT4g>o[dER"z>ǧaFS_Z-iwSRk9 8YE>VW>8_}J:$uɹa' x/-޼ g92 J\Ϻڷӂ-&% >?~qkfP*t^e43®\N+>V]d9u%Nw/Uj?:N֋:a? .@/iom9B57d-xk~Vw <'@ȝ?@K w6f8W]q5 +endstream +endobj +834 0 obj +<< +/Type /Page +/Parent 1 0 R +/MediaBox [ 0 0 595.275591 841.889764 ] +/Contents 833 0 R +/Resources 4 0 R +/Annots [ 835 0 R ] +/TrimBox [ 0 0 595.275591 841.889764 ] +/BleedBox [ 0 0 595.275591 841.889764 ] +>> +endobj +835 0 obj +<< +/Type /Annot +/Subtype /Link +/Rect [ 37.466457 771.023622 557.809134 35.138876 ] +/BS << +/W 0 +>> +/A << +/Type /Action +/S /URI +/URI (./Images/Figma/PosterV5.png) +>> +>> +endobj +836 0 obj +<< +/Filter /FlateDecode +/Length 664 +>> +stream +xUMo0 WL%IECK 0MO#%ʖ2̎cY( + ZS0u#U-6ugKt9l B*&#YDp;1xټ=Mߦ_ )DlĒ(ߞ-Ӆ5Wn1T Fw ;_1tO﹗;p9^x?@Ĭ(1d(7j:xal[4+HbCN`c 3؀W`@@p8v8Ug뜐׾Ԯ_vR#^~`I}x־Y+fU.vߠ8W_9~ªAGT|!~[xu>]F* kѹ)6#A{w{Z)ߓ-15iDI*jH B>Xcru%HTA?sfIlg[d,Æhi❔ǰ!:ޘu[6@zj7&p⺘aGy&>x;^uip XR gpɎn,}+\aj9z-.[5=l 2a,|r<ՙ,!] Orh0A}Ƣ?L% %r16H=t:Hig]sT;9߮N2U6<a= +endstream +endobj +837 0 obj +<< +/Type /Page +/Parent 1 0 R +/MediaBox [ 0 0 595.275591 841.889764 ] +/Contents 836 0 R +/Resources 4 0 R +/Annots [ 838 0 R 839 0 R ] +/TrimBox [ 0 0 595.275591 841.889764 ] +/BleedBox [ 0 0 595.275591 841.889764 ] +>> +endobj +838 0 obj +<< +/Type /Annot +/Subtype /Link +/Rect [ 297.637795 771.023622 297.637795 758.620422 ] +/BS << +/W 0 +>> +/A << +/Type /Action +/S /URI +/URI (./Images/Figma/PosterV5.png) +>> +>> +endobj +839 0 obj +<< /Type /Annot /Subtype /Link /Rect [ 37.466457 690.402822 37.466457 677.999622 ] @@ -12395,7 +12573,7 @@ endobj >> >> endobj -834 0 obj +840 0 obj << /Filter /FlateDecode /Length 383 @@ -12404,19 +12582,19 @@ stream xRn1 +vرTj{(\8n;Um%^0 'DZÑ SNs8JV cxmG!K5F$qh3c)J)d!|/7/ɈYd9Uvm{p.(>~3Sc1i|J6ǿ]Ŭn>V!hO} owˬtt_x:@0Ȭ*uWӂQi)áX'헻詌9|$b2T{器6(e#MӪoq 7ӿTQzt<Ӣgu0%~B Y84/q?TI;@7NtsCiy > > endstream endobj -835 0 obj +841 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 834 0 R +/Contents 840 0 R /Resources 4 0 R -/Annots [ 836 0 R ] +/Annots [ 842 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -836 0 obj +842 0 obj << /Type /Annot /Subtype /Link @@ -12431,7 +12609,7 @@ endobj >> >> endobj -837 0 obj +843 0 obj << /Filter /FlateDecode /Length 653 @@ -12442,19 +12620,19 @@ x :F5͇>/z r E2O}k.nc5}Ycרu9rPȱ/Ȗ04-qdN8&$i[r!08Q %|gqm15.J[h5Iovu}ZW9YK<|foNk; Id\4"9ֿu!.1\ɍpV?}k"3B;mZeynSdq*|LQ2pN"DwVc="L"lat> endobj -839 0 obj +845 0 obj << /Type /Annot /Subtype /Link @@ -12469,7 +12647,7 @@ endobj >> >> endobj -840 0 obj +846 0 obj << /Type /Annot /Subtype /Link @@ -12484,7 +12662,7 @@ endobj >> >> endobj -841 0 obj +847 0 obj << /Filter /FlateDecode /Length 384 @@ -12495,19 +12673,19 @@ x 1ڱ ^e$ endstream endobj -842 0 obj +848 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 841 0 R +/Contents 847 0 R /Resources 4 0 R -/Annots [ 843 0 R ] +/Annots [ 849 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -843 0 obj +849 0 obj << /Type /Annot /Subtype /Link @@ -12522,7 +12700,7 @@ endobj >> >> endobj -844 0 obj +850 0 obj << /Filter /FlateDecode /Length 1998 @@ -12547,19 +12725,19 @@ kؤԑ@  !d#GqύS~ [Kwa63a endstream endobj -845 0 obj +851 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 844 0 R +/Contents 850 0 R /Resources 4 0 R -/Annots [ 846 0 R 847 0 R 848 0 R 849 0 R ] +/Annots [ 852 0 R 853 0 R 854 0 R 855 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -846 0 obj +852 0 obj << /Type /Annot /Subtype /Link @@ -12574,7 +12752,7 @@ endobj >> >> endobj -847 0 obj +853 0 obj << /Type /Annot /Subtype /Link @@ -12589,7 +12767,7 @@ endobj >> >> endobj -848 0 obj +854 0 obj << /Type /Annot /Subtype /Link @@ -12604,7 +12782,7 @@ endobj >> >> endobj -849 0 obj +855 0 obj << /Type /Annot /Subtype /Link @@ -12619,7 +12797,7 @@ endobj >> >> endobj -850 0 obj +856 0 obj << /Filter /FlateDecode /Length 2374 @@ -12642,19 +12820,19 @@ Wc ŠIOF^I/-)FrMqatLպnԴ2͵y[[(My논- G္9?7BB@'~v59Ƥ}D{- :FX+s%(ipjCcߙ{9ݐ "+f +ޒUUvKOt$H@djd p- a"t/+ydq[R pXs׮P-)FOV x5={ݒ)serMu5&Ɯ- 6x CɵUl[Y% xnI R8zM7'c!\0q◿4dJX$ߪE1W򠶳d34b&U5]|M![%O'pa|nJR{H>Z?CI_hṰ7fDE#:GF"ڻw!/fʼn endstream endobj -851 0 obj +857 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 850 0 R +/Contents 856 0 R /Resources 4 0 R -/Annots [ 852 0 R ] +/Annots [ 858 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -852 0 obj +858 0 obj << /Type /Annot /Subtype /Link @@ -12669,7 +12847,7 @@ endobj >> >> endobj -853 0 obj +859 0 obj << /Filter /FlateDecode /Length 379 @@ -12681,19 +12859,19 @@ x TfGPNo 0?LQomL+jҀM_UcuϋGak> ng߱ٝ\#Rfg/ÅeǺӂ5&Ҥzגف}H-BcP1+ ְIP膑c}{"O72!&-Bh}{#e ŏyn2v(5&% hԍjcb(#!a endstream endobj -854 0 obj +860 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 853 0 R +/Contents 859 0 R /Resources 4 0 R -/Annots [ 855 0 R ] +/Annots [ 861 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -855 0 obj +861 0 obj << /Type /Annot /Subtype /Link @@ -12708,7 +12886,7 @@ endobj >> >> endobj -856 0 obj +862 0 obj << /Filter /FlateDecode /Length 497 @@ -12719,19 +12897,19 @@ x UE!! PE10NP ji5U">Xd%z8. }$~6!-Q\Ėc<+ ?}q'<#Gq{ǰg.i1/뵮4:s5.mc~E6L_WQQ_#TuΩ0]8 0:1XTx:?^(GQZVl7'@ʯBh!Ȅp?jM^c-v I¿`=Rv!-Z]η圮p endstream endobj -857 0 obj +863 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 856 0 R +/Contents 862 0 R /Resources 4 0 R -/Annots [ 858 0 R 859 0 R ] +/Annots [ 864 0 R 865 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -858 0 obj +864 0 obj << /Type /Annot /Subtype /Link @@ -12746,7 +12924,7 @@ endobj >> >> endobj -859 0 obj +865 0 obj << /Type /Annot /Subtype /Link @@ -12761,7 +12939,7 @@ endobj >> >> endobj -860 0 obj +866 0 obj << /Filter /FlateDecode /Length 375 @@ -12773,19 +12951,19 @@ T % BLfL)dJ V/HcȨ\r@_eq͇[[}/! zC`-53n|yw48 ʟ\T61 ybO,g endstream endobj -861 0 obj +867 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 860 0 R +/Contents 866 0 R /Resources 4 0 R -/Annots [ 862 0 R ] +/Annots [ 868 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -862 0 obj +868 0 obj << /Type /Annot /Subtype /Link @@ -12800,7 +12978,7 @@ endobj >> >> endobj -863 0 obj +869 0 obj << /Filter /FlateDecode /Length 385 @@ -12811,19 +12989,19 @@ B 1AȂGA(B]:C:}@뭘/s$mx{ j/ v~p?_^G=~z 7o endstream endobj -864 0 obj +870 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 863 0 R +/Contents 869 0 R /Resources 4 0 R -/Annots [ 865 0 R ] +/Annots [ 871 0 R ] /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -865 0 obj +871 0 obj << /Type /Annot /Subtype /Link @@ -12838,7 +13016,7 @@ endobj >> >> endobj -866 0 obj +872 0 obj << /Filter /FlateDecode /Length 2530 @@ -12854,18 +13032,18 @@ x b!(֓:9A55=}3/T\ endstream endobj -867 0 obj +873 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 866 0 R +/Contents 872 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -868 0 obj +874 0 obj << /Filter /FlateDecode /Length 2804 @@ -12881,18 +13059,18 @@ J >?M^:5uE"} >Վ endstream endobj -869 0 obj +875 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 868 0 R +/Contents 874 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -870 0 obj +876 0 obj << /Filter /FlateDecode /Length 990 @@ -12906,18 +13084,18 @@ x 4%][CKu]:wQ)'(/%.{dՉiye 5!*X_Nj3dhC9L2PdDA*a:RW"8^3փTn՗5AdO endstream endobj -871 0 obj +877 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 870 0 R +/Contents 876 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -872 0 obj +878 0 obj << /Filter /FlateDecode /Length 1625 @@ -12936,18 +13114,18 @@ x ;4&ޚؠy4FNoM/4ٯ/~|wMFeߟGlwts+#M&ӛ줭E0v+>~eHY׽gK\&CA% uRPAp`hT ,}^Kb endstream endobj -873 0 obj +879 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 872 0 R +/Contents 878 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -874 0 obj +880 0 obj << /Filter /FlateDecode /Length 1774 @@ -12962,18 +13140,18 @@ f* (L@_݈Fs̻wr{ E\p7/ endstream endobj -875 0 obj +881 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 874 0 R +/Contents 880 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -876 0 obj +882 0 obj << /Filter /FlateDecode /Length 3105 @@ -12997,18 +13175,18 @@ b c~> endobj -878 0 obj +884 0 obj << /Filter /FlateDecode /Length 2678 @@ -13030,18 +13208,18 @@ A! ?I1 endstream endobj -879 0 obj +885 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 878 0 R +/Contents 884 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -880 0 obj +886 0 obj << /Filter /FlateDecode /Length 2678 @@ -13062,18 +13240,18 @@ W C/EbIں劍A]&u䯍_Z&_\h*߀?1=$Wa?; ${E`)\cmnL endstream endobj -881 0 obj +887 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 880 0 R +/Contents 886 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -882 0 obj +888 0 obj << /Filter /FlateDecode /Length 699 @@ -13087,18 +13265,18 @@ Xr a1}Zt:քZH+'pVV_w endstream endobj -883 0 obj +889 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 882 0 R +/Contents 888 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -884 0 obj +890 0 obj << /Filter /FlateDecode /Length 1229 @@ -13112,18 +13290,18 @@ r2 z2zhU%CbvwU[uOwi߅pt~3B endstream endobj -885 0 obj +891 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 884 0 R +/Contents 890 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -886 0 obj +892 0 obj << /Filter /FlateDecode /Length 3296 @@ -13143,18 +13321,18 @@ s F{#6j @9'(SW\۩Z.Nt> 7 -\vS=+9)A?)DWx1OOM 1᥆ԤRZ"ߧDl}zM:J%r2D͏OQW_UUxnί*RNnޱkuƇ @:j]6 a%Ob\l,%țO ALa^ ,>(VDF_I|^C&,1laڕ endstream endobj -887 0 obj +893 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 886 0 R +/Contents 892 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -888 0 obj +894 0 obj << /Filter /FlateDecode /Length 2945 @@ -13170,18 +13348,18 @@ dp ˌqDiU-)R{n鍕@eN\\W;bsM\c}; +g%hfyh4?s3zB`.0^T3H%cd v˹pUS6rG?/?_Kcu3Ks㩞kcAI%0lRFMy9Ff0! Yng+/}"gQ!}۠TRG'#Zs)i0ͳ,E hi3NZl1ҥz2m[.YxmŧJa.)sw H v$E荦2vNufV JȽMyt"*SݨbIGهf%pDhBqFBϙEAblS?A땲O|W#@i$m'H9$rCz=FI1x8ء$#;sA4?]!<ⷳ[-:k)>'P%v yD/`^ x΢ۏ~r`f٧M;iPKJìp]ݾyt`E$>>t`^8K#ljޤ=nX僔ȫQEx-ˍihQv%!ɂ#f4 V{9T<h2~I #nlIz endstream endobj -889 0 obj +895 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 888 0 R +/Contents 894 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -890 0 obj +896 0 obj << /Filter /FlateDecode /Length 2947 @@ -13197,18 +13375,18 @@ f!6 6.s{qe]zc͐vI8G)-w`tp7.樰,(-TtP!afVpmO{߃~;uۇjӤ/%2d x7 )LLLM+ 6yw2\NlGWoK#_^2ݧJ7e?]o.ވQѳC*1}o{D`T`ӏ$.A> endobj -892 0 obj +898 0 obj << /Filter /FlateDecode /Length 1313 @@ -13225,18 +13403,18 @@ M bGx} ) u L.(?!@LRoLd >7;&L@FJU|֝!ͩ: &,K-?P'; vXw]JCBGk;> D^(2Bbt#pټF0A_q'qÜЪZ;kp :zJv w{c lXn%?U endstream endobj -893 0 obj +899 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 892 0 R +/Contents 898 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -894 0 obj +900 0 obj << /Filter /FlateDecode /Length 2668 @@ -13257,82 +13435,6 @@ x AXmlٵjEŰeCG7v4@a,2ce2 rKIQz6}Q} tFKH1WgQgwSL*ϯz-jjŧ)}|ȕO`rc^>?;䃲rܒ*HrofwӓX_wm>1> -endobj -896 0 obj -<< -/Filter /FlateDecode -/Length 763 ->> -stream -xVMo0 WL%)R@À@:d[ahziHYn`.Ħ%QԟR1p=4 (WR>_j{l|t1 fvFLȫ{'=ll|EbHsHb=9FݯfzW=] &`~޼؍' 308Df5j[5cddo\yb~znl|ͮu+\ί}0vsE'ڹ1Q~HcGK$%!!Ht2M4<4'~>o*%R6F7yU^=n.f ,Eu#\.9$tB#_WG̣>2qV-s- ZO[$Ě!4l{jTt}pߔ2+x{`-aHP0+<ж @oWoJVPp32mI $.- [-G)`~E(!|βZ{RẢVo-OFPS#j,M9. EbP -Q)YbY \Jח_]r -s(x]6b2^gvڇhǬ%UzJ5Y `W|^$jT54wC;$;@*4 7g#k*礳Ok}>9 e? -endstream -endobj -897 0 obj -<< -/Type /Page -/Parent 1 0 R -/MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 896 0 R -/Resources 4 0 R -/TrimBox [ 0 0 595.275591 841.889764 ] -/BleedBox [ 0 0 595.275591 841.889764 ] ->> -endobj -898 0 obj -<< -/Filter /FlateDecode -/Length 1621 ->> -stream -xYK7ϯ!, ;8lb!WKjw .IUz|%iagPgy:<|:h化+X|Wr:S.tv -{arF"2LC)n@T&)>?<EP~e<8Jb?8`2Â_?51ajQ&&Z[Hؙbjeue}YL793 84;˘Y8 _xgX!&η?o䛣Q9 -2S5Cfr|k}Z!J(H#vbAa3CnC+LcP;NG; -a@s9~> (v9 +tSI"mŠާPT#ɚ{`D|f 샄NbzLn:PXtz۹6y7a6|NiŦ4vGdW:O/[ 1D1: Ex -ٳܬS&t*R_'g_jQ+У݂ZcrobE߾<&-tcVY Z.rEkB"S}y7;>Xg£b2TEh ݕo\{7V0Xe!ۡ)pXjS`nc[XYPBckDz j42B'Mh|sC`y*N"1.F-z Ct@oM֣hsnqg#~0s%v]J)쾲,D}}@ߧܶ'F-TK%{vsFa@ej!bԎpه'H|I>vuKiq(4+/N-ZӴsM::1g$ ])eB - kMn2ĊySVbNļ2"Q2I2*[nnՉQ{5NQz^sG` -=orpAŸx}tɰcs*W -endstream -endobj -899 0 obj -<< -/Type /Page -/Parent 1 0 R -/MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 898 0 R -/Resources 4 0 R -/TrimBox [ 0 0 595.275591 841.889764 ] -/BleedBox [ 0 0 595.275591 841.889764 ] ->> -endobj -900 0 obj -<< -/Filter /FlateDecode -/Length 1657 ->> -stream -xY݋6߿ρ*ь>Xh $-4ڤ>nБ,Yw8wtHHZ|, çV|%58C Eރ4!npx{`y7 *`=G;Pzx,&QH3L@AN4 2VbA 60BrhEf0Փ/gbDJ$ncig2un?߽ -1|{>y5#YiLK=m_cuB:_ -E$b4t˼9Ӆy7 V3H(We𰚲ؖ3orO>g68,eEg%{me *[)Obmm!oRZS#0ۓ}.Fq;ᨭ"07qb2dF\`RlU:somf?6ՋIiRAgу4皋%\ZcCRJ , ;f3m a44m eR[}~cյm\+*c:miG;HIm#je/l}B:<|96R6ʋ\֗Q6@__3B0Ok-㍒+|Zersyu$;x=ë`=م7[s(r.A=Iʉ -<ǣ.BA_t!dƱ%f -|&o ! -endstream -endobj 901 0 obj << /Type /Page @@ -13347,10 +13449,11 @@ endobj 902 0 obj << /Filter /FlateDecode -/Length 1339 +/Length 763 >> stream -xXmo6 _ 8`Cv$ h|}se. R#؃|AS/ c`Bb|q1bL}h ;"CgC"B =1{~Խޣ_ݝ(*op21QneK&qӈ?vfi^ fC6yŝacv~35Cx33[`M"Jb>(@& ^ۻo?ы0|u~q.vl Jb)DpI[WZxu[kdBaTAWaZ~W[j zBChƅ. eD$/6.E8IY"+(v ZrVyFs~􀯑9srOi!rQ7VlC/)<잔5.zC/ijcɢ?$/r ުܟd$"G}A8?Eڟfdq@]5h*_AZry0pʇ",ҸW֜&wPkіSs5m}6r8?fǢfO2LU_t@rz' G9KUl Ӂ #bqyv1r1õŵ˻|9?vڱC'{R(OrM#3'V`dQKf[O^Oo'8R#0(ZdR .YI h f +xVMo0 WL%)R@À@:d[ahziHYn`.Ħ%QԟR1p=4 (WR>_j{l|t1 fvFLȫ{'=ll|EbHsHb=9FݯfzW=] &`~޼؍' 308Df5j[5cddo\yb~znl|ͮu+\ί}0vsE'ڹ1Q~HcGK$%!!Ht2M4<4'~>o*%R6F7yU^=n.f ,Eu#\.9$tB#_WG̣>2qV-s- ZO[$Ě!4l{jTt}pߔ2+x{`-aHP0+<ж @oWoJVPp32mI $.- [-G)`~E(!|βZ{RẢVo-OFPS#j,M9. EbP +Q)YbY \Jח_]r -s(x]6b2^gvڇhǬ%UzJ5Y `W|^$jT54wC;$;@*4 7g#k*礳Ok}>9 e? endstream endobj 903 0 obj @@ -13367,17 +13470,17 @@ endobj 904 0 obj << /Filter /FlateDecode -/Length 2802 +/Length 1621 >> stream -xێ_= X MM 4ŶIQa][`zHM%[h,vjď~E:xt|``0Ki8%>:[w4LHjJ׏>I0k ;m_o^9/9\j5HQ).n `?ЁS_WbjZ%kB3#HkC2L^+!z>f&j{?uR#@WR0w|/?}{/e|#Q,C/ңs= _~>7aj+2)kU{TǮ_M/hj+3PҦO 1 kHtpjti^KNٛVE:ƞ LrtPc]9״^=tD"3`,}.ZCt{-NS;oo\us >Y2eܕ$%d~5m)% w_;Rh}a7.&ONIJ });ا9anlWFu4I${$!u*~j0RJUn/=#&QJPQ^4 UjX,@5p#R3z|{sLl3HN^\;^c C|[&zYi@.95H7q8xQ jeπ GvTGj9r82 )͋].1(s8O\ -bmdܖ!`.d/xm'5h=e~ZT$v7Sdknh1Q o"bkTU}{`3Q13,qjΐ{O!σi UDCMNQ)rdT8T{r%,FMo`ܐ2 K*zyR0 TѳG^lZ%V\^yL<+-lLİ{o=h XJXd[ih %%wG"eOA$YSF6a@X{!.?LRH%S)- LR2N$Vuo ⵴oee7)e &81u1yOG{#S6w @_\g(}b(aH^hP=Wz䔊# -م[3vY̹R·qmC!UDyublU W:p%s8ov)nf;ޛN>LǭW^,8mpJ|SL{ u ՅtuZ`0KicUA9LYXdG 0z/D̽!B*SQnCnumiWh1x䅀&*3Ix3k rp)`L19%>"KsD V"K -sׅUswYwiq kו1]q跓N𖚻hzQDo%B̢ (G†@I\XWy"QBeV:?fu+%F Aa^V}U;"cZâڵNwăsӯ Pt-^H J[YJ^7FTh_&qތq̒tZP2,_. tzI@ 5u7@ΔfsƭezҰC6lexc4_fz稙j\)?ڭNFxuRa@0#'FrФ#E&Ӷ/H.!'7V'_\m j lH+F5pWjQrm0ЮjT"@H_ \:ݱ"i ^Y~C4uD rwـO i/[>+RM`Vy+)cvpz8)M2U|y# J-&()6K͌i>SYRذi?PTXN{\LJpBjJ)Z^|3?ǹQyU/FxZ8\"8SP28,ˠ[b2:"$fh$0[<`6 kkVٛZ8C8n֗QWW*og/]NAX -TO}$l wJa+p#c#q|>M=ewڿ`Q[ \*g#_W/wG25,n3_3ݼ:0{]t622!ciMs㨟c);G /ŴA+mBL^_r|(Tb2\݈gq +xYK7ϯ!, ;8lb!WKjw .IUz|%iagPgy:<|:h化+X|Wr:S.tv +{arF"2LC)n@T&)>?<EP~e<8Jb?8`2Â_?51ajQ&&Z[Hؙbjeue}YL793 84;˘Y8 _xgX!&η?o䛣Q9 +2S5Cfr|k}Z!J(H#vbAa3CnC+LcP;NG; +a@s9~> (v9 +tSI"mŠާPT#ɚ{`D|f 샄NbzLn:PXtz۹6y7a6|NiŦ4vGdW:O/[ 1D1: Ex -ٳܬS&t*R_'g_jQ+У݂ZcrobE߾<&-tcVY Z.rEkB"S}y7;>Xg£b2TEh ݕo\{7V0Xe!ۡ)pXjS`nc[XYPBckDz j42B'Mh|sC`y*N"1.F-z Ct@oM֣hsnqg#~0s%v]J)쾲,D}}@ߧܶ'F-TK%{vsFa@ej!bԎpه'H|I>vuKiq(4+/N-ZӴsM::1g$ ])eB + kMn2ĊySVbNļ2"Q2I2*[nnՉQ{5NQz^sG` +=orpAŸx}tɰcs*W endstream endobj 905 0 obj @@ -13394,6 +13497,81 @@ endobj 906 0 obj << /Filter /FlateDecode +/Length 1657 +>> +stream +xY݋6߿ρ*ь>Xh $-4ڤ>nБ,Yw8wtHHZ|, çV|%58C Eރ4!npx{`y7 *`=G;Pzx,&QH3L@AN4 2VbA 60BrhEf0Փ/gbDJ$ncig2un?߽ +1|{>y5#YiLK=m_cuB:_ +E$b4t˼9Ӆy7 V3H(We𰚲ؖ3orO>g68,eEg%{me *[)Obmm!oRZS#0ۓ}.Fq;ᨭ"07qb2dF\`RlU:somf?6ՋIiRAgу4皋%\ZcCRJ , ;f3m a44m eR[}~cյm\+*c:miG;HIm#je/l}B:<|96R6ʋ\֗Q6@__3B0Ok-㍒+|Zersyu$;x=ë`=م7[s(r.A=Iʉ +<ǣ.BA_t!dƱ%f +|&o ! +endstream +endobj +907 0 obj +<< +/Type /Page +/Parent 1 0 R +/MediaBox [ 0 0 595.275591 841.889764 ] +/Contents 906 0 R +/Resources 4 0 R +/TrimBox [ 0 0 595.275591 841.889764 ] +/BleedBox [ 0 0 595.275591 841.889764 ] +>> +endobj +908 0 obj +<< +/Filter /FlateDecode +/Length 1339 +>> +stream +xXmo6 _ 8`Cv$ h|}se. R#؃|AS/ c`Bb|q1bL}h ;"CgC"B =1{~Խޣ_ݝ(*op21QneK&qӈ?vfi^ fC6yŝacv~35Cx33[`M"Jb>(@& ^ۻo?ы0|u~q.vl Jb)DpI[WZxu[kdBaTAWaZ~W[j zBChƅ. eD$/6.E8IY"+(v ZrVyFs~􀯑9srOi!rQ7VlC/)<잔5.zC/ijcɢ?$/r ުܟd$"G}A8?Eڟfdq@]5h*_AZry0pʇ",ҸW֜&wPkіSs5m}6r8?fǢfO2LU_t@rz' G9KUl Ӂ #bqyv1r1õŵ˻|9?vڱC'{R(OrM#3'V`dQKf[O^Oo'8R#0(ZdR .YI h f +endstream +endobj +909 0 obj +<< +/Type /Page +/Parent 1 0 R +/MediaBox [ 0 0 595.275591 841.889764 ] +/Contents 908 0 R +/Resources 4 0 R +/TrimBox [ 0 0 595.275591 841.889764 ] +/BleedBox [ 0 0 595.275591 841.889764 ] +>> +endobj +910 0 obj +<< +/Filter /FlateDecode +/Length 2802 +>> +stream +xێ_= X MM 4ŶIQa][`zHM%[h,vjď~E:xt|``0Ki8%>:[w4LHjJ׏>I0k ;m_o^9/9\j5HQ).n `?ЁS_WbjZ%kB3#HkC2L^+!z>f&j{?uR#@WR0w|/?}{/e|#Q,C/ңs= _~>7aj+2)kU{TǮ_M/hj+3PҦO 1 kHtpjti^KNٛVE:ƞ LrtPc]9״^=tD"3`,}.ZCt{-NS;oo\us >Y2eܕ$%d~5m)% w_;Rh}a7.&ONIJ });ا9anlWFu4I${$!u*~j0RJUn/=#&QJPQ^4 UjX,@5p#R3z|{sLl3HN^\;^c C|[&zYi@.95H7q8xQ jeπ GvTGj9r82 )͋].1(s8O\ +bmdܖ!`.d/xm'5h=e~ZT$v7Sdknh1Q o"bkTU}{`3Q13,qjΐ{O!σi UDCMNQ)rdT8T{r%,FMo`ܐ2 K*zyR0 TѳG^lZ%V\^yL<+-lLİ{o=h XJXd[ih %%wG"eOA$YSF6a@X{!.?LRH%S)- LR2N$Vuo ⵴oee7)e &81u1yOG{#S6w @_\g(}b(aH^hP=Wz䔊# +م[3vY̹R·qmC!UDyublU W:p%s8ov)nf;ޛN>LǭW^,8mpJ|SL{ u ՅtuZ`0KicUA9LYXdG 0z/D̽!B*SQnCnumiWh1x䅀&*3Ix3k rp)`L19%>"KsD V"K +sׅUswYwiq kו1]q跓N𖚻hzQDo%B̢ (G†@I\XWy"QBeV:?fu+%F Aa^V}U;"cZâڵNwăsӯ Pt-^H J[YJ^7FTh_&qތq̒tZP2,_. tzI@ 5u7@ΔfsƭezҰC6lexc4_fz稙j\)?ڭNFxuRa@0#'FrФ#E&Ӷ/H.!'7V'_\m j lH+F5pWjQrm0ЮjT"@H_ \:ݱ"i ^Y~C4uD rwـO i/[>+RM`Vy+)cvpz8)M2U|y# J-&()6K͌i>SYRذi?PTXN{\LJpBjJ)Z^|3?ǹQyU/FxZ8\"8SP28,ˠ[b2:"$fh$0[<`6 kkVٛZ8C8n֗QWW*og/]NAX +TO}$l wJa+p#c#q|>M=ewڿ`Q[ \*g#_W/wG25,n3_3ݼ:0{]t622!ciMs㨟c);G /ŴA+mBL^_r|(Tb2\݈gq +endstream +endobj +911 0 obj +<< +/Type /Page +/Parent 1 0 R +/MediaBox [ 0 0 595.275591 841.889764 ] +/Contents 910 0 R +/Resources 4 0 R +/TrimBox [ 0 0 595.275591 841.889764 ] +/BleedBox [ 0 0 595.275591 841.889764 ] +>> +endobj +912 0 obj +<< +/Filter /FlateDecode /Length 2941 >> stream @@ -13410,18 +13588,18 @@ I Y\ 7vR/-q6*#۝t͹:ubr;mAKUF9bpK4umε3^on7fz9w,m!|T^&aa5VVfFqn5Y-kɀs&zSv4S7o1W[J$jjӝ?|F tMZ5Z k o/PKJ}<[كSwsf"Nq×a×q? '&^bP$7:uP> endobj -908 0 obj +914 0 obj << /Filter /FlateDecode /Length 1849 @@ -13440,18 +13618,18 @@ zhi> InZY-.;<`;w @ox[g*6'/|f`|~w*凎0Xb1U/f1oSp"NqC+xu0HAg0I%v,qKhE]xt/Y/]L?|*[?Z){k|? endstream endobj -909 0 obj +915 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 908 0 R +/Contents 914 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -910 0 obj +916 0 obj << /Filter /FlateDecode /Length 2857 @@ -13470,18 +13648,18 @@ Hz qz56z%PgxAP[6x%si [G0WҶ?Edl U640rڵ4J<+9i&mgu $@*U @vK#nbҲ?TP翁^V.C{yr! !MtfeL8iFӎIq\宁)ք2-\ݚ yZpd7De'O^<GMgXf7{6\5TF`ioV_f:^}+}<څwrHSLK-)\n5*$}O~b#ycy6j)[}$7_/,oj!Edd8}Ӈ`EzPėh#.Zojc; >q243~dd`5 xAF{^ ֥12csHjtbLkOLJ? endstream endobj -911 0 obj +917 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 910 0 R +/Contents 916 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -912 0 obj +918 0 obj << /Filter /FlateDecode /Length 2430 @@ -13498,18 +13676,18 @@ sG eOSuh ̟y #N+K_m-~ endstream endobj -913 0 obj +919 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 912 0 R +/Contents 918 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -914 0 obj +920 0 obj << /Filter /FlateDecode /Length 1567 @@ -13527,18 +13705,18 @@ x .tGoF %%"Aǒg Xvq=L"tCz]. C%ݽzðjQ,G.D?tS endstream endobj -915 0 obj +921 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 914 0 R +/Contents 920 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -916 0 obj +922 0 obj << /Filter /FlateDecode /Length 2796 @@ -13557,18 +13735,18 @@ y HL.PS#_/X@pln@H.#fΧPhbO62Mɭ+ῷRa endstream endobj -917 0 obj +923 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 916 0 R +/Contents 922 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -918 0 obj +924 0 obj << /Filter /FlateDecode /Length 759 @@ -13581,18 +13759,18 @@ $ +-W*З^)0C=zlAɸg5z]X'ٲQ9W6aô#ͲAr;P:~CJa6"?hYRŲFEu:C򹍉DiA7=`  |cRMVj+WNfRۮ+ <֙d endstream endobj -919 0 obj +925 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 918 0 R +/Contents 924 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -920 0 obj +926 0 obj << /Filter /FlateDecode /Length 2321 @@ -13614,18 +13792,18 @@ Sv WE٨]y6]A:8^}n9gIMy`ˡ!|FUڌb]Yy gWVw}: ^v@]}+J*Jҡ.^e\S)zU@g[Ɩ{ZY śۗօ] QrWā$gR$BejK=t >ޅ\! b|zn,bo4>ŧ>qbl񾡽x^ endstream endobj -921 0 obj +927 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 920 0 R +/Contents 926 0 R /Resources 4 0 R /TrimBox [ 0 0 595.275591 841.889764 ] /BleedBox [ 0 0 595.275591 841.889764 ] >> endobj -922 0 obj +928 0 obj << /Filter /FlateDecode /Length 3304 @@ -13642,85 +13820,6 @@ a {Hzv7j|ЦvG꿆6=xs/}7zXM, tG V)f^PzĶaF1R߭KEKyZN ~OU 㹹օ+:꒤Ѽc}6.U8)\9&:*K3d}NHf-<$}-vBņGXmx;Mbф ;tۀroMF?s梚4Z;ft$3R27SyO^7a ֖KGA^ǃ;1%+|_"Ov6NK-jW!A= +*0GoλjC@Ȍ:LJ0SCxndDrHHS].}j8:=]=A@u> -endobj -924 0 obj -<< -/Filter /FlateDecode -/Length 2003 ->> -stream -xˎ6!9|=h!Ŷ P`\6?!EJ,ʴMvK#>͙oss?}}ށ=F7{nw -IZZhE'A**%H+vy]o:GRz(}~{s@μPT4Aj^C,/vੜ(/Yғ -RHIiG(&b8v,v Q0蕁6zy1Kߒ<h |$ӎA( HP>0;rL F`i~ -N;w?Q5}@ܨk<5ձg:?=z7<0.D-## ӳCwԇs,8/Sif$]_u)S |u(ݏM<ߏ<%;"*[sOZ0Ao*`V፷hEs6t -c6s gnZq[&'#״Y0*I%ȮC_r /Vͭr쵒Ip]3Аht{ [izW6\'ԒdKܢ S<`y#ח|7l $yrH `(ўt-Hח}u:H9 ]_6{ \'vBRw岭%uwN-\Z_J{iX./ILka;h[{թ:Ëj}yUX7HjUJU -endstream -endobj -925 0 obj -<< -/Type /Page -/Parent 1 0 R -/MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 924 0 R -/Resources 4 0 R -/TrimBox [ 0 0 595.275591 841.889764 ] -/BleedBox [ 0 0 595.275591 841.889764 ] ->> -endobj -926 0 obj -<< -/Filter /FlateDecode -/Length 3042 ->> -stream -x\݋$߿b )J%@l8's3w\RRKꞑF s#R}TR>(;-y}`/w -{| Ȱ;^p`AkeZ *>ǻ9X`Akctpb }~ ei܏D\|%bR*+!(&չHwHN 6Uca585\&ws*V l?|N"hFÇw_oIa?)='P6LWsOjЪQxÇ >{7Fm \|lޣ[5HM -͚dXİ?~|IȄ`'%gUc&]'uVNok%@DMS$6RUV| e[JW}/SZu̟lذ"O lɕ{KaS_ތ.(0Z#0nyoV[)9EY_=H3Z+P Mo(h#bDto)'FO\G=\NRvd -\y0^)9kʿk9xf8K7q.0d5tMh [Xa\&.rMXәsEs"2YJ /c%uq>^;m8ə8ÆI^lk4d([S䠬 Z. "1*l\kWQ́B$vћ6#zsPFKl٬ -b_Bicd4y$3`e:8U^_c.} J+>NW0cXa)3o.+ V>hf̘%H9\e|m*~s7/JLPMV "CC*yݣ$YDݪ'j&+ƕߕ} -S>lqR )K6ĠXg6EXsP,#pa%U=A1Asc9VbVإ}[G+meux0y4iDGeObD%nel]gN<?ZsQ&4$7*dQ) f–wuٲ/3~ UT^fa6=؂ *`)3fSAM^E3RMk-Hoϋ 2y&Džv-`hܰn?sv̈L[+`AnV3>T`4+%V&ͣrC Ʃvg΀aX.e~k6rfdnـ^Yũ"N?o^!Qv:9r\~Ak `NXMDݑXA!@UA-ѴJ)ՠ]@us$R) X4%er'0UJcn|zX$^"T 0g%Ecs71Ԟրq; >?JF7Y%mÆu¸=!4z ;j$+SuRlOg^7ʛh.Th3)p_ em^$ܶoHTg-1;4k)Ր?Պ-M"}HNVs-*ͩA|:cnSLXIy\t)&JvlO8(\XYw]P/)n:gW@(8ܐjO #\Yeus N7M\pHJx^E^GuBћLxUcܟS~~kuQr4c{"(s4WW+t1VIjb>@8<Ӌ2fVlq1̡Lqg?L1% -*&I5^I%v__!oUR]n#zԧ.{m&c"mǓϼ46'Zѧ׭ǟ:ڶs 96Ǔϲ,8YZ!r7(MhxZP,/U*5%_G$ʉFREEH^)~QS?[^O%Ի5Ҭ>3;UzRyR:E-2d)Əoݟ!MkXRL 3zơhx > -endobj -928 0 obj -<< -/Filter /FlateDecode -/Length 2467 ->> -stream -xۮ_,@`>^"<@:H%ZeIrWkKȹpfl+y ?oΚ/;g'r#.vv чCJ\$6D}k]|n509}|ݫ,-U^弼G ظBa#m+*Yi3{}6?ﰰ=hmJjPyͷϿw7^AKO Q "[,6>&NJ}׭\szi~SwZedIkcNZ[u踾pȻsLQ>ch ->\%dcS&rN<1NVsF԰V?u09MƷأ%cx.YF?`T\yvg^{4C\Q$kcI/9fňϩsrOg'ls;y ۧkF{07˱csud|pC00êeb(nf[qM@Z[ {Ҡx9ʖHgK2zV27{8z>$tU+nW D6/z0ye>]׬ Z(T{_]l^ȦIMWm20[@%eG8~l3A[ ZjcA.c۪fKǖ%Bc^l(I+/׽olAvI+Y{̜--C@kf՗ғn+:ǖK&S)(̕{h[w.\ٯO5!Ml.+JwEKl7'df.hy -!=+B@] ̻<5~ޕٕ Brz8|83VRA&Z>'bC19 ẗtRijr98.uSvG JJ KY Dw ƃrJ?-T:?O$vF$X΄]ogk1WlM7ZJxB0 -39~Y50XY܏"6LDc#(?7%CWusdJ6SI8>ݷgP8~cO^ 圇`}3*@sNT[hEEQ `Cv)B _yGsо7)|+Y/ -endstream -endobj 929 0 obj << /Type /Page @@ -13735,18 +13834,15 @@ endobj 930 0 obj << /Filter /FlateDecode -/Length 2358 +/Length 2003 >> stream -x\[~,!@`>ض>=S$@)QE˲,'-~ƣ᷃JGiȁ ={$7 r>z] pJ9Zcc<ƿ>|sBPǟw8BR\W W3$ҦZAr/\cA#:2v1{GEAkd TV/o~㇣r|Q,ꡥRlyikȫW4~!@"AkM\Xa^U=+1!_yk]uS|]qю2j͟eƱ}2M=ʲ`ʴ_-*&ͣ)8(הuzv^AάUl~eY}a(8$^s.]u餱`\Y=붵aJ8Keu6SyWzx*|nkedSEwGz n[%!QFI@{Ƞn{1gqLQ"׿:Z/ Ӽ@Yb&TūPΙb~X4agm -Kzg缴 !^glЄŁ'sӊh1Xϙ:a?,c̚P-=`5R4H4x'L0dbSɮY4,uSM1ئ籊I>/C_v9M%Xq([,r4W^#vcFr=o^tjq6]\? R" /S}CKW0~\To d -Ao53ϋbf#{Өo C8^=ݏO#52Gern2Q<:dom/V|Ċ(`PF|ܦ"OCHGds/-K[  `au -]}VBN6IVb8a [Eo*Nu@og1֎m~'n iPɗ); [4σtKDt۠͢~&[*5wmȫ"3AxƁ)$ ->/^ރ ^rts.yԊ9ohL`g8}t^\, 8D~5̯kUk^S)|NâP8m3SonqNw}/cr+t7:67q{E_}ҖwtI<ҚHNt]el XbaBUI._ -;V]˿ (k -SmunV>̿㏑[$`4_%9d8svZ"`hRYckg "Vߖ5xg~eY )Wu( z "O_9妄ϿmРye SJx! J<5cJ/ F7<=Q1级-u6RsGknJGE@vJDž#M܎i -\д]qBWZxi? <(~[)Je&0K۝1YL8?F8* j -GJV~}E59֟p '?Qr `s }%!=xg mvWxһ Y1vYWpd ٺ -e-3 ^2rȿҒ}@/U+%q#}Z{Ò} yK1zKJNN=80rs|2L__E;Oo>r;*҈$9` ';SrbFKCpm*y,yopUl *Lվ~ҐVpfwx0q<-Ʊ~OGIBҿ;<%I024}55OBDJ$</Q\Ot1L4rm-Ka@_/?d.2"{>}P×c.A +xˎ6!9|=h!Ŷ P`\6?!EJ,ʴMvK#>͙oss?}}ށ=F7{nw +IZZhE'A**%H+vy]o:GRz(}~{s@μPT4Aj^C,/vੜ(/Yғ +RHIiG(&b8v,v Q0蕁6zy1Kߒ<h |$ӎA( HP>0;rL F`i~ +N;w?Q5}@ܨk<5ձg:?=z7<0.D-## ӳCwԇs,8/Sif$]_u)S |u(ݏM<ߏ<%;"*[sOZ0Ao*`V፷hEs6t +c6s gnZq[&'#״Y0*I%ȮC_r /Vͭr쵒Ip]3Аht{ [izW6\'ԒdKܢ S<`y#ח|7l $yrH `(ўt-Hח}u:H9 ]_6{ \'vBRw岭%uwN-\Z_J{iX./ILka;h[{թ:Ëj}yUX7HjUJU endstream endobj 931 0 obj @@ -13763,18 +13859,17 @@ endobj 932 0 obj << /Filter /FlateDecode -/Length 2598 +/Length 3042 >> stream -x\͎)e, , Y`6$ 9==mݼ?"%JhVw&Ɩ͟cUYxwZF~= `_l1__OI6gA[֝- -Ji{=SN ψR3B <~>}wAs_ϯځ &6HRZ3s}A"!?dMԡY+g :ɂZ9PxjJ?r_83iN+V󧏫J± Bx.d.)}KzPvhoUWEx4}%qҫmu12FPdw3]!үb/[iiANrm}H\=g'7#L;J1==o>mKt/FƗIy\L&-*>E?Nc^X|l8H0/E5z2x]& SA Qym󶡰ߝPYixkV*EcX]&TuaA8..n$u'j`:k []biux<'Wڪ"1*iÃYp'3WY.kBLM sEnQ: 3Z|'7,z{M|sm͊p>Bѥ뎠f;_&ӻMDvQ ߱úoK dK@X2-8e+[ttG|[D݅i]obv3BZ//rPTgFc*myh,dޒ;adH9U󦰼FPbdp$895KVY=BP&<ҷz@K] ֡mÒ<\R}@ϟ^W}İGnS%fd`V+?Fu@[u$# $+m? PF18g߉D:ZRVFdZ߫HG$`v#IAq(Lb<6G~[iPݶ3[уzbS41({0~=x5f !s?)΋1fM&jіlEtU9r)fZ5b1#G9alv6҈u$^'ǔ%ja2v@h{+`S%lk3S~Y1Gke@9c0e&P:v8֘ځÙ423Ciffy@d3LF%=d4,nD=U`b Т|4g:b61Ố HCCwD݆kf%LKwFH.qb ُj#ғ^r N{ve LP9YDoxB~P.;{ԡ6CMpt(v^UJHk0t?J@cxѠd?}_ru#C%UW =oHgbYA{è̈́ YqWTQѦu.Yr:;lIlf$L -`'_a;FIvpu?c?6c? hxlì- -P~量2Z +8|\aCYfJnoWq8?r`9X M ځa?oeL &r@bUqqE:"ցo$2ߡ?iG*t҂r>x׬}S -(Dw>DL.sq}I;'$o8TYb9%iYqGc=/ )C/#j7b,y+UE@mA9G ݎ }/^ZHgεW_!-&|0f"ح _//K"x _:o|pd0[%M-gYU3?(;-y}`/w +{| Ȱ;^p`AkeZ *>ǻ9X`Akctpb }~ ei܏D\|%bR*+!(&չHwHN 6Uca585\&ws*V l?|N"hFÇw_oIa?)='P6LWsOjЪQxÇ >{7Fm \|lޣ[5HM +͚dXİ?~|IȄ`'%gUc&]'uVNok%@DMS$6RUV| e[JW}/SZu̟lذ"O lɕ{KaS_ތ.(0Z#0nyoV[)9EY_=H3Z+P Mo(h#bDto)'FO\G=\NRvd +\y0^)9kʿk9xf8K7q.0d5tMh [Xa\&.rMXәsEs"2YJ /c%uq>^;m8ə8ÆI^lk4d([S䠬 Z. "1*l\kWQ́B$vћ6#zsPFKl٬ +b_Bicd4y$3`e:8U^_c.} J+>NW0cXa)3o.+ V>hf̘%H9\e|m*~s7/JLPMV "CC*yݣ$YDݪ'j&+ƕߕ} +S>lqR )K6ĠXg6EXsP,#pa%U=A1Asc9VbVإ}[G+meux0y4iDGeObD%nel]gN<?ZsQ&4$7*dQ) f–wuٲ/3~ UT^fa6=؂ *`)3fSAM^E3RMk-Hoϋ 2y&Džv-`hܰn?sv̈L[+`AnV3>T`4+%V&ͣrC Ʃvg΀aX.e~k6rfdnـ^Yũ"N?o^!Qv:9r\~Ak `NXMDݑXA!@UA-ѴJ)ՠ]@us$R) X4%er'0UJcn|zX$^"T 0g%Ecs71Ԟրq; >?JF7Y%mÆu¸=!4z ;j$+SuRlOg^7ʛh.Th3)p_ em^$ܶoHTg-1;4k)Ր?Պ-M"}HNVs-*ͩA|:cnSLXIy\t)&JvlO8(\XYw]P/)n:gW@(8ܐjO #\Yeus N7M\pHJx^E^GuBћLxUcܟS~~kuQr4c{"(s4WW+t1VIjb>@8<Ӌ2fVlq1̡Lqg?L1% +*&I5^I%v__!oUR]n#zԧ.{m&c"mǓϼ46'Zѧ׭ǟ:ڶs 96Ǔϲ,8YZ!r7(MhxZP,/U*5%_G$ʉFREEH^)~QS?[^O%Ի5Ҭ>3;UzRyR:E-2d)Əoݟ!MkXRL 3zơhx > stream -x\K6Wl⫀ 0}@آDڴ,-M$ñ%QbZxT'A3󏇟 -U3Ը'ylwG6`BG&I)g5Xdl^eqa`;!x&uxen*G蝕n7#;HM|N@kd TV󏇯 rXZWl2gD|INrrmӎ}Oc;>b烺"WJXJ8>MK5a]׼˧pPDcs,?~4 $";rAv>-PL܋H HٱnD65 -=֍Sm@WeLId45V'r*EEeje=I&Г*Ra|'U@LcaG}KOcop+ ިur&b%Q/Z)Y+~[vNRZ5ەڬIfA}-uVqYؤ̸<8+> ,jf+-O)yOqP:&," ژ4,@Up%~Fs0+tpTY3W kMFD8nʥ4N:sl~P[HMs0 Ҳ\5WIu͆`I V+aXF ú /Y>l]9L>X.٢6"[TcF-b2j6Xf6io5]}ݩf;ȍZWͅ8ڱlY IUJ_ɻ솲 M?5RO dŗHG6q =WofN+π@pG]eբD:k O0v :-6jjStT܇3F:ڠC=,sDY4Ss-a7}7 n< -uk cv]Mr~˕H}JqDZr,z]t\ \^@y"Inb3P KC{'x33|#7sd髺LLh B{>ƽύ},`F9Zx^j;'1Ik6Zs!UsR+Y^ 9 v=TIchYڃOr>0\0S{FҎ}0citֵ73պ8Z)hLQ -[2K,]暮z :jneq{]d b?@nZeA{aF7 A (hrn 5?RLoK&fxH3m 8?f0k@5neu Q-a`$ ݛs2 &DNii2@H`Ɖ⚀hi&f$M}+&r~kn LS~Uyyq=G<{ [특qrCeFa9Jp%mžĖI6ѐ¹@CΙ %L) g5oT{(l̎258V]:#q{raŋw^ctdzL4镢EoY i͢^sZ?X##W!}grb7z RUvQ&T_+擓^T_P5FgP e;FPzKr|*}SW4vѣʞ{l:gXd4G*|=4>V:س NE* !?ۓ,!gSNe-kfgIB#?,C^7 =~z^TJh+he_JTMYuqxsmWیm[ Ȉ@%T_]"KTfgFܮ^rIk518~[- jD%;y*֕-+wt+ubq=eu"N*ΞIo(jb(~b'gŌbX"=*=>;0ӵhݱrb i݊U`ڱ/P5-(E[z:@Y ӗKvkXƻ/K/%G^"<@:H%ZeIrWkKȹpfl+y ?oΚ/;g'r#.vv чCJ\$6D}k]|n509}|ݫ,-U^弼G ظBa#m+*Yi3{}6?ﰰ=hmJjPyͷϿw7^AKO Q "[,6>&NJ}׭\szi~SwZedIkcNZ[u踾pȻsLQ>ch +>\%dcS&rN<1NVsF԰V?u09MƷأ%cx.YF?`T\yvg^{4C\Q$kcI/9fňϩsrOg'ls;y ۧkF{07˱csud|pC00êeb(nf[qM@Z[ {Ҡx9ʖHgK2zV27{8z>$tU+nW D6/z0ye>]׬ Z(T{_]l^ȦIMWm20[@%eG8~l3A[ ZjcA.c۪fKǖ%Bc^l(I+/׽olAvI+Y{̜--C@kf՗ғn+:ǖK&S)(̕{h[w.\ٯO5!Ml.+JwEKl7'df.hy +!=+B@] ̻<5~ޕٕ Brz8|83VRA&Z>'bC19 ẗtRijr98.uSvG JJ KY Dw ƃrJ?-T:?O$vF$X΄]ogk1WlM7ZJxB0 +39~Y50XY܏"6LDc#(?7%CWusdJ6SI8>ݷgP8~cO^ 圇`}3*@sNT[hEEQ `Cv)B _yGsо7)|+Y/ endstream endobj 935 0 obj @@ -13814,15 +13913,18 @@ endobj 936 0 obj << /Filter /FlateDecode -/Length 1948 +/Length 2358 >> stream -x[_o6 Oq_`,)}趇 `CrIhQ|Ία[R56mK$E2Q!/8x?|}!;NN埵}Yl -@*Z`g-߻D (ADSO_>^?.B vt LfDxN<;kO;NSz*uTz1<0pᢄ>^˲ ݘӏk!z%Bg)99; CP#cqw?۽vVWcm"C Fq-wmvAv)(5]:[ f 5 -@fPP<#1Q<73ʚfFUA# N W3`YlLMԠ<_^B<*nmƻ6k J#3|^|Of;B3-&(E䅖B<|0} rQ\A_qd2'^TyaibDnvLKєmـ.H$6J'&9 R%G'o>)TجMYA3Šbe^M0F&5l- <Ïqŕ$." -8rcU—ϛ Z^*B /Os3YbVWAY3QiV 8 ޝd y(L3jFk5!N6ifU׸3Ev>@!X88I+7{!qnC&g7E#b}>S9tE76]UwpXW5C}ǚ`*Vvxfpv'"dH9ݟ^+'v}i;Wjv( -Wj]EwO]T.vSE=aiNJEzi #BBU*38beu<ˋ!-(AyfZx5y|\zY#//Ռ3D.Mh߮оm j5޴WܮքDlX b U`c{m U>u22Ƹ4gi/l\z,ŶgAxw(^[ Kq%΁ڋ:kK2fNޕق`ork zҎ^ ZݼtCo8LPrAXyM7"h򧂚{HkgsΤp8 -"̟ÏnQ60AԮ?z!i/2 6K=Ϫa>ȑi,)3:/!\A9}Z7hAuLdN =c%ǖdHfJa&g? Bp +x\[~,!@`>ض>=S$@)QE˲,'-~ƣ᷃JGiȁ ={$7 r>z] pJ9Zcc<ƿ>|sBPǟw8BR\W W3$ҦZAr/\cA#:2v1{GEAkd TV/o~㇣r|Q,ꡥRlyikȫW4~!@"AkM\Xa^U=+1!_yk]uS|]qю2j͟eƱ}2M=ʲ`ʴ_-*&ͣ)8(הuzv^AάUl~eY}a(8$^s.]u餱`\Y=붵aJ8Keu6SyWzx*|nkedSEwGz n[%!QFI@{Ƞn{1gqLQ"׿:Z/ Ӽ@Yb&TūPΙb~X4agm +Kzg缴 !^glЄŁ'sӊh1Xϙ:a?,c̚P-=`5R4H4x'L0dbSɮY4,uSM1ئ籊I>/C_v9M%Xq([,r4W^#vcFr=o^tjq6]\? R" /S}CKW0~\To d +Ao53ϋbf#{Өo C8^=ݏO#52Gern2Q<:dom/V|Ċ(`PF|ܦ"OCHGds/-K[  `au +]}VBN6IVb8a [Eo*Nu@og1֎m~'n iPɗ); [4σtKDt۠͢~&[*5wmȫ"3AxƁ)$ ->/^ރ ^rts.yԊ9ohL`g8}t^\, 8D~5̯kUk^S)|NâP8m3SonqNw}/cr+t7:67q{E_}ҖwtI<ҚHNt]el XbaBUI._ +;V]˿ (k +SmunV>̿㏑[$`4_%9d8svZ"`hRYckg "Vߖ5xg~eY )Wu( z "O_9妄ϿmРye SJx! J<5cJ/ F7<=Q1级-u6RsGknJGE@vJDž#M܎i +\д]qBWZxi? <(~[)Je&0K۝1YL8?F8* j +GJV~}E59֟p '?Qr `s }%!=xg mvWxһ Y1vYWpd ٺ +e-3 ^2rȿҒ}@/U+%q#}Z{Ò} yK1zKJNN=80rs|2L__E;Oo>r;*҈$9` ';SrbFKCpm*y,yopUl *Lվ~ҐVpfwx0q<-Ʊ~OGIBҿ;<%I024}55OBDJ$</Q\Ot1L4rm-Ka@_/?d.2"{>}P×c.A endstream endobj 937 0 obj @@ -13839,6 +13941,82 @@ endobj 938 0 obj << /Filter /FlateDecode +/Length 2598 +>> +stream +x\͎)e, , Y`6$ 9==mݼ?"%JhVw&Ɩ͟cUYxwZF~= `_l1__OI6gA[֝- +Ji{=SN ψR3B <~>}wAs_ϯځ &6HRZ3s}A"!?dMԡY+g :ɂZ9PxjJ?r_83iN+V󧏫J± Bx.d.)}KzPvhoUWEx4}%qҫmu12FPdw3]!үb/[iiANrm}H\=g'7#L;J1==o>mKt/FƗIy\L&-*>E?Nc^X|l8H0/E5z2x]& SA Qym󶡰ߝPYixkV*EcX]&TuaA8..n$u'j`:k []biux<'Wڪ"1*iÃYp'3WY.kBLM sEnQ: 3Z|'7,z{M|sm͊p>Bѥ뎠f;_&ӻMDvQ ߱úoK dK@X2-8e+[ttG|[D݅i]obv3BZ//rPTgFc*myh,dޒ;adH9U󦰼FPbdp$895KVY=BP&<ҷz@K] ֡mÒ<\R}@ϟ^W}İGnS%fd`V+?Fu@[u$# $+m? PF18g߉D:ZRVFdZ߫HG$`v#IAq(Lb<6G~[iPݶ3[уzbS41({0~=x5f !s?)΋1fM&jіlEtU9r)fZ5b1#G9alv6҈u$^'ǔ%ja2v@h{+`S%lk3S~Y1Gke@9c0e&P:v8֘ځÙ423Ciffy@d3LF%=d4,nD=U`b Т|4g:b61Ố HCCwD݆kf%LKwFH.qb ُj#ғ^r N{ve LP9YDoxB~P.;{ԡ6CMpt(v^UJHk0t?J@cxѠd?}_ru#C%UW =oHgbYA{è̈́ YqWTQѦu.Yr:;lIlf$L +`'_a;FIvpu?c?6c? hxlì- +P~量2Z +8|\aCYfJnoWq8?r`9X M ځa?oeL &r@bUqqE:"ցo$2ߡ?iG*t҂r>x׬}S +(Dw>DL.sq}I;'$o8TYb9%iYqGc=/ )C/#j7b,y+UE@mA9G ݎ }/^ZHgεW_!-&|0f"ح _//K"x _:o|pd0[%M-gYU3?> +endobj +940 0 obj +<< +/Filter /FlateDecode +/Length 2425 +>> +stream +x\K6Wl⫀ 0}@آDڴ,-M$ñ%QbZxT'A3󏇟 +U3Ը'ylwG6`BG&I)g5Xdl^eqa`;!x&uxen*G蝕n7#;HM|N@kd TV󏇯 rXZWl2gD|INrrmӎ}Oc;>b烺"WJXJ8>MK5a]׼˧pPDcs,?~4 $";rAv>-PL܋H HٱnD65 -=֍Sm@WeLId45V'r*EEeje=I&Г*Ra|'U@LcaG}KOcop+ ިur&b%Q/Z)Y+~[vNRZ5ەڬIfA}-uVqYؤ̸<8+> ,jf+-O)yOqP:&," ژ4,@Up%~Fs0+tpTY3W kMFD8nʥ4N:sl~P[HMs0 Ҳ\5WIu͆`I V+aXF ú /Y>l]9L>X.٢6"[TcF-b2j6Xf6io5]}ݩf;ȍZWͅ8ڱlY IUJ_ɻ솲 M?5RO dŗHG6q =WofN+π@pG]eբD:k O0v :-6jjStT܇3F:ڠC=,sDY4Ss-a7}7 n< +uk cv]Mr~˕H}JqDZr,z]t\ \^@y"Inb3P KC{'x33|#7sd髺LLh B{>ƽύ},`F9Zx^j;'1Ik6Zs!UsR+Y^ 9 v=TIchYڃOr>0\0S{FҎ}0citֵ73պ8Z)hLQ +[2K,]暮z :jneq{]d b?@nZeA{aF7 A (hrn 5?RLoK&fxH3m 8?f0k@5neu Q-a`$ ݛs2 &DNii2@H`Ɖ⚀hi&f$M}+&r~kn LS~Uyyq=G<{ [특qrCeFa9Jp%mžĖI6ѐ¹@CΙ %L) g5oT{(l̎258V]:#q{raŋw^ctdzL4镢EoY i͢^sZ?X##W!}grb7z RUvQ&T_+擓^T_P5FgP e;FPzKr|*}SW4vѣʞ{l:gXd4G*|=4>V:س NE* !?ۓ,!gSNe-kfgIB#?,C^7 =~z^TJh+he_JTMYuqxsmWیm[ Ȉ@%T_]"KTfgFܮ^rIk518~[- jD%;y*֕-+wt+ubq=eu"N*ΞIo(jb(~b'gŌbX"=*=>;0ӵhݱrb i݊U`ڱ/P5-(E[z:@Y ӗKvkXƻ/K/%G> +endobj +942 0 obj +<< +/Filter /FlateDecode +/Length 1948 +>> +stream +x[_o6 Oq_`,)}趇 `CrIhQ|Ία[R56mK$E2Q!/8x?|}!;NN埵}Yl +@*Z`g-߻D (ADSO_>^?.B vt LfDxN<;kO;NSz*uTz1<0pᢄ>^˲ ݘӏk!z%Bg)99; CP#cqw?۽vVWcm"C Fq-wmvAv)(5]:[ f 5 +@fPP<#1Q<73ʚfFUA# N W3`YlLMԠ<_^B<*nmƻ6k J#3|^|Of;B3-&(E䅖B<|0} rQ\A_qd2'^TyaibDnvLKєmـ.H$6J'&9 R%G'o>)TجMYA3Šbe^M0F&5l- <Ïqŕ$." +8rcU—ϛ Z^*B /Os3YbVWAY3QiV 8 ޝd y(L3jFk5!N6ifU׸3Ev>@!X88I+7{!qnC&g7E#b}>S9tE76]UwpXW5C}ǚ`*Vvxfpv'"dH9ݟ^+'v}i;Wjv( +Wj]EwO]T.vSE=aiNJEzi #BBU*38beu<ˋ!-(AyfZx5y|\zY#//Ռ3D.Mh߮оm j5޴WܮքDlX b U`c{m U>u22Ƹ4gi/l\z,ŶgAxw(^[ Kq%΁ڋ:kK2fNޕق`ork zҎ^ ZݼtCo8LPrAXyM7"h򧂚{HkgsΤp8 +"̟ÏnQ60AԮ?z!i/2 6K=Ϫa>ȑi,)3:/!\A9}Z7hAuLdN =c%ǖdHfJa&g? Bp +endstream +endobj +943 0 obj +<< +/Type /Page +/Parent 1 0 R +/MediaBox [ 0 0 595.275591 841.889764 ] +/Contents 942 0 R +/Resources 4 0 R +/TrimBox [ 0 0 595.275591 841.889764 ] +/BleedBox [ 0 0 595.275591 841.889764 ] +>> +endobj +944 0 obj +<< +/Filter /FlateDecode /Length 2343 >> stream @@ -13854,79 +14032,6 @@ r F 6ΘVF~l/' ,-OjEB^~=|> -endobj -940 0 obj -<< -/Filter /FlateDecode -/Length 2109 ->> -stream -x[n$7S D%Q$ 09$lgSH@(]]եe=5I$?R(~0B1~?qOK<tL`#뼞`O;I!kAȘc`Ug#BL=iz>\@YG![dv9sj`լǏ÷?~͑uX=~yY>~>YA5N="5LעOϱM0{~곊'8=u>㇫00"JЁn¨n7h9ƓAwŢ%z8KEzاAj4E`Q^A?'/fy1Ė3_gri?VUSc$aCω*,hy.9_ȓ'6H?kر^UR 2B)Ѧ&H%֤Qwh4E)/&UӤsu <×)ͨu t<}oއM쫋K®!ԁ>6ɰ#թ4ĢpBQ4sT;_9+P8S'Gj1=r~YM㰳f,[zytP_󞢤 RD@h~|h|~o(؋A䌠<$OBtE xQ#r([~3r5 r l# B+i 3X7ANӾX2{Qquik |BI۾xֶɖQgE]hAjcIe(ʠoнulNۖ`Q&ge=+K 4;|#]raJ%"ZV6fy=):_r"KW۴{  ШX|F+cT`64r ɺq2Wkrfū[US)w\ -u|PB+ EdPCrUq[}Vi7[y0y_Gm" (I^kh$g]:&1W$-HTYWlg%-)!DjPGO}Ja-s>%2<7ċQGJ1 -[`ڵSǬ*: 8_tr@eXR#TOQ*޳Lb\߿,d^=;{*4&,P -endstream -endobj -941 0 obj -<< -/Type /Page -/Parent 1 0 R -/MediaBox [ 0 0 595.275591 841.889764 ] -/Contents 940 0 R -/Resources 4 0 R -/TrimBox [ 0 0 595.275591 841.889764 ] -/BleedBox [ 0 0 595.275591 841.889764 ] ->> -endobj -942 0 obj -<< -/Filter /FlateDecode -/Length 2182 ->> -stream -x\͎)e, !@$9l0=mݼ?DjEQj3{8mIYDF_1 - !Ĥ4A`O$?cN3}s㟴XզiH݄^"kp.mҽ&KU{Yq,0e"ImF┓剂 f|m5=AkmDs'Aq]b@gԋk3gDc&<-`&- ZcqY;Ń$!SU 5hnf9Lu[/Ŷysa@`"4I-L>-8fJK۠9jf8pCpyw9^aDqw=9hba͉]>~ZY{cQ<_7Ddҍf.Cj!xO\8Rq婉Kg"x•DK.S(utO94aގ|!}3ym7%d[>Չl4ǫ%)ɭzgphcUKNKC%ڪkvCT}MKbܷnfn/jv 8[Ȁ^Qwڋ=H&aҳHǎ LKoDf^\VcFW>mEJy:oPqc֤8 _vl7.͍ۨmOEb+R5e}:m>Ը /p\$ t=}i W&~@o% 0nkLRlm2 -A~Am.1e*Y1NbŠk|yk`a%sNJB)53Sp %w>[)XxYS`%6OÐ纰K N2xi5^S e+ĺa6 nd}zn y1/ vORiS;R(jrZjKyЌaӵV(xpHV]P|MD"I`MTl qpֶ8N s^L=6 :q7'xǁzڿWeG@ßwxw(;0c;$Z,Df,Lw|CԐveA-42fp-k3 n,}E@R_}?tԪh)_}ya> -endobj -944 0 obj -<< -/Filter /FlateDecode -/Length 2407 ->> -stream -x\A# WT%R -!Ŷ }) Fь=z -@+'Uj~Vy}Gc@_~9\*;bZb†[\kNí<޻2ޑ5 :̌?2ZԊ`>~~}JA]߽fʅa1f(U<;;ʻM;\sB1]G?݃r6~qO}"tq~ɴWw#OxH}L }}P }3O/]y]pu::1q 2$1^bpXR,҈8Uɒ#v}\& 'L-QCZ$6=08RQzì9uRL"hD$|KZE_ad; vƭ~o6h%c6H2ehF59EIȽVlx Vs0ݶ<s~&].s|qf@fagxmUg7VK`TZK51݌LRIcR%,V&K@3dgKD̚6$ l~u1oMigJV$f 4=y8kI(? pZI:8 ')^ݬnpb$93YW{%#qi#` ^{ڞ lzdɽƮ`NM -?ĿqWR(4ܦ9]V$]ab^}VfJiEtdK_BؾQs9\UIڍZ --. ɹё99Rx3E:8U]R{?D5=Mmzt2tJy$&3Rʄlxl[2p^p ,R^OEʗt ,K@9=kI6Ӷѫ[S%Fȼ{i0&MnoO7 U= \H^GnŸdD3Q28%dȓ\50SŽ:i(%Ge&z}߻[zwpIZ܌[PeLGfڔZt5ۏƛ uAPRn2S{6;ڏmۇGIIoO`%=o[tyV8;]SsXKo22asU촕T^fp܁Ti{ѸeHJyάJ9n/Y ŪcCsZzKqrf+srW;ŕ*zU^,̖7bYS2]DCSsՇzMtKWΝsNaցjF;fӓbO@pq jtan&GrFySTP6lH϶ʞk02 >Mcʿ1ؼ}>M xƁ%20+|: QD8q:9vTk.'sFD8jO;}l_ ;/%ܾ2R5}J{?}]nh[Tf<:ۦXjC(k}?3~[fy߆%Yo; -9?x׃w|hCށ0nWcWWJOcm$u@TeeKP5VKf'GclNc׃IZ1oڳM6hdmMV()`mŢ'l,"]6.ijha޵"Z1?oQlo[VAR-56d<l4-cǕ*L;n/bEvŲ$+K`[yڛrg نH+i[Lةf묧wOە%}H*x[h`uPb {uMPŗ?c[hcQ7k$cPkyח!u 耝W -iаYmyݖ4!uzeqKO5:yFiS5W1ZkölHm^nHn%p" ³$.鬤tgŃރAArC]Ƞ%0N M0V -bUj7srto~0{ -endstream -endobj 945 0 obj << /Type /Page @@ -13941,15 +14046,14 @@ endobj 946 0 obj << /Filter /FlateDecode -/Length 1559 +/Length 2109 >> stream -xZ͎6 )UD) ȡ@ m)?@N.I;hh -t'zhzo@#9`}ag|ɷy6q#=7[CnGa6[ұzq'eykR8;$D܉ 2@>u޾ןl?^+ '&R}X<@V#Co(BY&N^={wVx$[~.3w`T|KѸYRAR -(*S 㠪ty!&u0Bt3R+it8 < )Py CgqN˚Y4"-&N[`566LP,w;SE8=5;}^NA -[o -+58#5;3Pml噢R_sQ HOW^i(Fja7'iCw.V-[h|0g%Hr0gaK kfQ۠hi3> +&ϕ`ٽ1ӱh-)m5юCV5C$6^m .ց}(;e(׮X 0kt%j,`-{61",G- yqd#&yfCŸ|3O&$l 2wq1ϣ*WF|$,)^i̷Gژ}dѷwsWXm0wwO+w .`( um>g9s]vǗ^ւJ="Xc(n7aL;{P  ۻK(g2YFضaZp ;Lٙy׆KgxSXV24Q\;ֺ[z_ 9g7JJ(Fj3ϙ.D Yh5`FO|K($jv_eL)#KcG!C1GPuӻB!NyԊqΉd4cXsȒqLqqХSd:"nR7jI@ךv7~1kwMjI;WJv;ٟ|%M OܢO@E؊==mNc ~lkDԗCl^B 'Ro8"Ql6)`MH?|ږ+Bڷ~/M5[q:#  -vo^xTdcϓiX +x[n$7S D%Q$ 09$lgSH@(]]եe=5I$?R(~0B1~?qOK<tL`#뼞`O;I!kAȘc`Ug#BL=iz>\@YG![dv9sj`լǏ÷?~͑uX=~yY>~>YA5N="5LעOϱM0{~곊'8=u>㇫00"JЁn¨n7h9ƓAwŢ%z8KEzاAj4E`Q^A?'/fy1Ė3_gri?VUSc$aCω*,hy.9_ȓ'6H?kر^UR 2B)Ѧ&H%֤Qwh4E)/&UӤsu <×)ͨu t<}oއM쫋K®!ԁ>6ɰ#թ4ĢpBQ4sT;_9+P8S'Gj1=r~YM㰳f,[zytP_󞢤 RD@h~|h|~o(؋A䌠<$OBtE xQ#r([~3r5 r l# B+i 3X7ANӾX2{Qquik |BI۾xֶɖQgE]hAjcIe(ʠoнulNۖ`Q&ge=+K 4;|#]raJ%"ZV6fy=):_r"KW۴{  ШX|F+cT`64r ɺq2Wkrfū[US)w\ -u|PB+ EdPCrUq[}Vi7[y0y_Gm" (I^kh$g]:&1W$-HTYWlg%-)!DjPGO}Ja-s>%2<7ċQGJ1 +[`ڵSǬ*: 8_tr@eXR#TOQ*޳Lb\߿,d^=;{*4&,P endstream endobj 947 0 obj @@ -13966,15 +14070,13 @@ endobj 948 0 obj << /Filter /FlateDecode -/Length 3053 +/Length 2182 >> stream -x[K#Wl 4u:v if|,vujiIdf{5"dWo5 J)SG,oࢊdR>F1:ltNypN/8cv*jS<c&s,xl>.4-5F{c̮.Ye_a?-ܼZ9[B;WƐ`* H FUǂ/ ڬ„Pn}_`:?|<|~:uoڤ;yct9ICؓzxUpҔULVɸy KTAdu>JHpTT;'釨9NF ]/CkFAڃ&hNf:O櫃YsMGcFL׮Z%gK_GB| u;"l͹Qx.gfhfr3Ph_y5Md-ATLz[*l[_ ZYcb(.l,}Jv!EhJ2[ -^p/m6AI+oѳ]߱Կ4lT8H)Cy^R[!uZa;f3 ^WyTtEX/jqm+1q> sj0꾶 #*m֫4y*&9Z~_y*.H3=5!*mw 믇&7`9(pմu)TIR@ppc& p98~[ڕ{7UIA4nMhOlrcl]4FBFWl}2IJvWf«&3'*AqƤHԘ?4kq{Q(Y%^޵Ӆ{:BCrz|kX̵gȌgyf̹ eа<Ŋúz l3M̦"RL/M J'- 1tM1+žv-@JPRےDyŒWlzDIKny<e2` 1ǎÈXfu4anqHӠ^O' f;3/I7^(PNUC /sTil<.e"YY^,8o8pKumvmhJLH]}b{{> &;̐ } ګyle:A239lbVJp*+MXÂfe2.:23EMUiFlESȆ+pF6_cߥQkv+b5on6!PS3F(K9&1Jnsq-OYNYN)fw sznwYṶ[/J^JP4vӜg^v-nB:&=99? R ֹ5?ھ7a BL*e,:GHwVEDv^@43&ˆOW\9Ѷ."ŃUr͛o{Ծ;5)' pYI3f~HLp6* ʔkDkn;@ϴ#zoFm}ݹj1tOz|m`,wˠ̜5ʢrW頻t30u73s$26,8? ]awa!^%pJx* D!*\(l8j -<ᑉ -Z(ndOG=uyǥ@X6"=Vމ1s1Ժdbo>k zfF 318j) < 3O&J݊J^biV_Es cU bڿF(m%ﭯ9*vɏY09-nԊūSlN-Th*=T }>.> -C]ܿ3s~zG9Xr`*a;(@xz!5ح >+DuS[9I,6tn.mn'o! Jb%l>3pKSz椰zYVd2XtdKvOevDU99w'_}S6jSO-x&b?joJ)iwlcy{\!v)[8Jr[g 5vFً{"I\$ITwt}^.Ɋ @a&f`Vo bX ׾ MlO{ ]6?/#ؙ3Kh\Dv/w P:9+?p*Z3j99C\ D˟".)R+O^zW+Mmҫzq.ByҠhW_?8zuO7su2ݾ^X~5by56'}4:r6!9U(?s'*w]~s0.n!P7vA`"nu VGx,1oT' +x\͎)e, !@$9l0=mݼ?DjEQj3{8mIYDF_1 + !Ĥ4A`O$?cN3}s㟴XզiH݄^"kp.mҽ&KU{Yq,0e"ImF┓剂 f|m5=AkmDs'Aq]b@gԋk3gDc&<-`&- ZcqY;Ń$!SU 5hnf9Lu[/Ŷysa@`"4I-L>-8fJK۠9jf8pCpyw9^aDqw=9hba͉]>~ZY{cQ<_7Ddҍf.Cj!xO\8Rq婉Kg"x•DK.S(utO94aގ|!}3ym7%d[>Չl4ǫ%)ɭzgphcUKNKC%ڪkvCT}MKbܷnfn/jv 8[Ȁ^Qwڋ=H&aҳHǎ LKoDf^\VcFW>mEJy:oPqc֤8 _vl7.͍ۨmOEb+R5e}:m>Ը /p\$ t=}i W&~@o% 0nkLRlm2 +A~Am.1e*Y1NbŠk|yk`a%sNJB)53Sp %w>[)XxYS`%6OÐ纰K N2xi5^S e+ĺa6 nd}zn y1/ vORiS;R(jrZjKyЌaӵV(xpHV]P|MD"I`MTl qpֶ8N s^L=6 :q7'xǁzڿWeG@ßwxw(;0c;$Z,Df,Lw|CԐveA-42fp-k3 n,}E@R_}?tԪh)_}ya> +stream +x\A# WT%R +!Ŷ }) Fь=z +@+'Uj~Vy}Gc@_~9\*;bZb†[\kNí<޻2ޑ5 :̌?2ZԊ`>~~}JA]߽fʅa1f(U<;;ʻM;\sB1]G?݃r6~qO}"tq~ɴWw#OxH}L }}P }3O/]y]pu::1q 2$1^bpXR,҈8Uɒ#v}\& 'L-QCZ$6=08RQzì9uRL"hD$|KZE_ad; vƭ~o6h%c6H2ehF59EIȽVlx Vs0ݶ<s~&].s|qf@fagxmUg7VK`TZK51݌LRIcR%,V&K@3dgKD̚6$ l~u1oMigJV$f 4=y8kI(? pZI:8 ')^ݬnpb$93YW{%#qi#` ^{ڞ lzdɽƮ`NM +?ĿqWR(4ܦ9]V$]ab^}VfJiEtdK_BؾQs9\UIڍZ +-. ɹё99Rx3E:8U]R{?D5=Mmzt2tJy$&3Rʄlxl[2p^p ,R^OEʗt ,K@9=kI6Ӷѫ[S%Fȼ{i0&MnoO7 U= \H^GnŸdD3Q28%dȓ\50SŽ:i(%Ge&z}߻[zwpIZ܌[PeLGfڔZt5ۏƛ uAPRn2S{6;ڏmۇGIIoO`%=o[tyV8;]SsXKo22asU촕T^fp܁Ti{ѸeHJyάJ9n/Y ŪcCsZzKqrf+srW;ŕ*zU^,̖7bYS2]DCSsՇzMtKWΝsNaցjF;fӓbO@pq jtan&GrFySTP6lH϶ʞk02 >Mcʿ1ؼ}>M xƁ%20+|: QD8q:9vTk.'sFD8jO;}l_ ;/%ܾ2R5}J{?}]nh[Tf<:ۦXjC(k}?3~[fy߆%Yo; +9?x׃w|hCށ0nWcWWJOcm$u@TeeKP5VKf'GclNc׃IZ1oڳM6hdmMV()`mŢ'l,"]6.ijha޵"Z1?oQlo[VAR-56d<l4-cǕ*L;n/bEvŲ$+K`[yڛrg نH+i[Lةf묧wOە%}H*x[h`uPb {uMPŗ?c[hcQ7k$cPkyח!u 耝W -iаYmyݖ4!uzeqKO5:yFiS5W1ZkölHm^nHn%p" ³$.鬤tgŃރAArC]Ƞ%0N M0V +bUj7srto~0{ +endstream endobj 951 0 obj << -/Title (1. Rapport Track Trends V1.0) -/Dest [ 186 0 R /XYZ 37.466457 759.623622 0 ] -/Count 55 -/Prev 950 0 R -/First 952 0 R -/Last 1006 0 R -/Next 1007 0 R -/Parent 1053 0 R +/Type /Page +/Parent 1 0 R +/MediaBox [ 0 0 595.275591 841.889764 ] +/Contents 950 0 R +/Resources 4 0 R +/TrimBox [ 0 0 595.275591 841.889764 ] +/BleedBox [ 0 0 595.275591 841.889764 ] >> endobj 952 0 obj << -/Title (1.1 Introduction) -/Dest [ 192 0 R /XYZ 37.466457 731.488422 0 ] -/Count 3 -/First 953 0 R -/Last 955 0 R -/Parent 951 0 R -/Next 956 0 R +/Filter /FlateDecode +/Length 1559 >> +stream +xZ͎6 )UD) ȡ@ m)?@N.I;hh +t'zhzo@#9`}ag|ɷy6q#=7[CnGa6[ұzq'eykR8;$D܉ 2@>u޾ןl?^+ '&R}X<@V#Co(BY&N^={wVx$[~.3w`T|KѸYRAR +(*S 㠪ty!&u0Bt3R+it8 < )Py CgqN˚Y4"-&N[`566LP,w;SE8=5;}^NA +[o ++58#5;3Pml噢R_sQ HOW^i(Fja7'iCw.V-[h|0g%Hr0gaK kfQ۠hi3> +&ϕ`ٽ1ӱh-)m5юCV5C$6^m .ց}(;e(׮X 0kt%j,`-{61",G- yqd#&yfCŸ|3O&$l 2wq1ϣ*WF|$,)^i̷Gژ}dѷwsWXm0wwO+w .`( um>g9s]vǗ^ւJ="Xc(n7aL;{P  ۻK(g2YFضaZp ;Lٙy׆KgxSXV24Q\;ֺ[z_ 9g7JJ(Fj3ϙ.D Yh5`FO|K($jv_eL)#KcG!C1GPuӻB!NyԊqΉd4cXsȒqLqqХSd:"nR7jI@ךv7~1kwMjI;WJv;ٟ|%M OܢO@E؊==mNc ~lkDԗCl^B 'Ro8"Ql6)`MH?|ږ+Bڷ~/M5[q:#  +vo^xTdcϓiX +endstream endobj 953 0 obj << -/Title -/Dest [ 192 0 R /XYZ 37.466457 685.328922 0 ] -/Count 0 -/Parent 952 0 R -/Next 954 0 R +/Type /Page +/Parent 1 0 R +/MediaBox [ 0 0 595.275591 841.889764 ] +/Contents 952 0 R +/Resources 4 0 R +/TrimBox [ 0 0 595.275591 841.889764 ] +/BleedBox [ 0 0 595.275591 841.889764 ] >> endobj 954 0 obj << -/Title (1.1.2 Abstract) -/Dest [ 192 0 R /XYZ 37.466457 131.212566 0 ] -/Count 0 -/Prev 953 0 R -/Parent 952 0 R -/Next 955 0 R +/Filter /FlateDecode +/Length 3053 >> +stream +x[K#Wl 4u:v if|,vujiIdf{5"dWo5 J)SG,oࢊdR>F1:ltNypN/8cv*jS<c&s,xl>.4-5F{c̮.Ye_a?-ܼZ9[B;WƐ`* H FUǂ/ ڬ„Pn}_`:?|<|~:uoڤ;yct9ICؓzxUpҔULVɸy KTAdu>JHpTT;'釨9NF ]/CkFAڃ&hNf:O櫃YsMGcFL׮Z%gK_GB| u;"l͹Qx.gfhfr3Ph_y5Md-ATLz[*l[_ ZYcb(.l,}Jv!EhJ2[ +^p/m6AI+oѳ]߱Կ4lT8H)Cy^R[!uZa;f3 ^WyTtEX/jqm+1q> sj0꾶 #*m֫4y*&9Z~_y*.H3=5!*mw 믇&7`9(pմu)TIR@ppc& p98~[ڕ{7UIA4nMhOlrcl]4FBFWl}2IJvWf«&3'*AqƤHԘ?4kq{Q(Y%^޵Ӆ{:BCrz|kX̵gȌgyf̹ eа<Ŋúz l3M̦"RL/M J'- 1tM1+žv-@JPRےDyŒWlzDIKny<e2` 1ǎÈXfu4anqHӠ^O' f;3/I7^(PNUC /sTil<.e"YY^,8o8pKumvmhJLH]}b{{> &;̐ } ګyle:A239lbVJp*+MXÂfe2.:23EMUiFlESȆ+pF6_cߥQkv+b5on6!PS3F(K9&1Jnsq-OYNYN)fw sznwYṶ[/J^JP4vӜg^v-nB:&=99? R ֹ5?ھ7a BL*e,:GHwVEDv^@43&ˆOW\9Ѷ."ŃUr͛o{Ծ;5)' pYI3f~HLp6* ʔkDkn;@ϴ#zoFm}ݹj1tOz|m`,wˠ̜5ʢrW頻t30u73s$26,8? ]awa!^%pJx* D!*\(l8j +<ᑉ +Z(ndOG=uyǥ@X6"=Vމ1s1Ժdbo>k zfF 318j) < 3O&J݊J^biV_Es cU bڿF(m%ﭯ9*vɏY09-nԊūSlN-Th*=T }>.> +C]ܿ3s~zG9Xr`*a;(@xz!5ح >+DuS[9I,6tn.mn'o! Jb%l>3pKSz椰zYVd2XtdKvOevDU99w'_}S6jSO-x&b?joJ)iwlcy{\!v)[8Jr[g 5vFً{"I\$ITwt}^.Ɋ @a&f`Vo bX ׾ MlO{ ]6?/#ؙ3Kh\Dv/w P:9+?p*Z3j99C\ D˟".)R+O^zW+Mmҫzq.ByҠhW_?8zuO7su2ݾ^X~5by56'}4:r6!9U(?s'*w]~s0.n!P7vA`"nu VGx,1oT' +endstream endobj 955 0 obj << -/Title (1.1.3 Description du besoin) -/Dest [ 198 0 R /XYZ 37.466457 588.076422 0 ] -/Count 0 -/Prev 954 0 R -/Parent 952 0 R +/Type /Page +/Parent 1 0 R +/MediaBox [ 0 0 595.275591 841.889764 ] +/Contents 954 0 R +/Resources 4 0 R +/TrimBox [ 0 0 595.275591 841.889764 ] +/BleedBox [ 0 0 595.275591 841.889764 ] >> endobj 956 0 obj << -/Title (1.2 Cahier des charges) -/Dest [ 198 0 R /XYZ 37.466457 192.543222 0 ] -/Count 4 -/Prev 952 0 R -/First 957 0 R -/Last 960 0 R -/Parent 951 0 R -/Next 961 0 R +/Title (Documentation Track Trends) +/Dest [ 6 0 R /XYZ 62.546457 480.091682 0 ] +/Count 0 +/Next 957 0 R +/Parent 1061 0 R >> endobj 957 0 obj << -/Title (1.2.1 Projet) -/Dest [ 200 0 R /XYZ 37.466457 771.023622 0 ] -/Count 0 -/Parent 956 0 R -/Next 958 0 R +/Title (1. Rapport Track Trends V1.0) +/Dest [ 192 0 R /XYZ 37.466457 759.623622 0 ] +/Count 57 +/Prev 956 0 R +/First 958 0 R +/Last 1014 0 R +/Next 1015 0 R +/Parent 1061 0 R >> endobj 958 0 obj << -/Title -/Dest [ 200 0 R /XYZ 37.466457 471.845622 0 ] -/Count 0 -/Prev 957 0 R -/Parent 956 0 R -/Next 959 0 R +/Title (1.1 Introduction) +/Dest [ 198 0 R /XYZ 37.466457 731.488422 0 ] +/Count 3 +/First 959 0 R +/Last 961 0 R +/Parent 957 0 R +/Next 962 0 R >> endobj 959 0 obj << -/Title (1.2.3 Cas d'utilisation) -/Dest [ 207 0 R /XYZ 37.466457 142.669266 0 ] +/Title +/Dest [ 198 0 R /XYZ 37.466457 685.328922 0 ] /Count 0 -/Prev 958 0 R -/Parent 956 0 R +/Parent 958 0 R /Next 960 0 R >> endobj 960 0 obj << -/Title -/Dest [ 212 0 R /XYZ 37.466457 479.548422 0 ] +/Title (1.1.2 Abstract) +/Dest [ 198 0 R /XYZ 37.466457 131.212566 0 ] /Count 0 /Prev 959 0 R -/Parent 956 0 R +/Parent 958 0 R +/Next 961 0 R >> endobj 961 0 obj << -/Title -/Dest [ 212 0 R /XYZ 37.466457 293.549622 0 ] +/Title (1.1.3 Description du besoin) +/Dest [ 204 0 R /XYZ 37.466457 522.959622 0 ] /Count 0 -/Prev 956 0 R -/Parent 951 0 R -/Next 962 0 R +/Prev 960 0 R +/Parent 958 0 R >> endobj 962 0 obj << -/Title -/Dest [ 212 0 R /XYZ 37.466457 219.482922 0 ] -/Count 19 -/Prev 961 0 R +/Title (1.2 Cahier des charges) +/Dest [ 204 0 R /XYZ 37.466457 127.426422 0 ] +/Count 4 +/Prev 958 0 R /First 963 0 R -/Last 963 0 R -/Parent 951 0 R -/Next 982 0 R +/Last 966 0 R +/Parent 957 0 R +/Next 967 0 R >> endobj 963 0 obj << -/Title -/Dest [ 214 0 R /XYZ 37.466457 771.023622 0 ] -/Count 18 -/First 964 0 R -/Last 979 0 R +/Title (1.2.1 Projet) +/Dest [ 206 0 R /XYZ 37.466457 718.310022 0 ] +/Count 0 /Parent 962 0 R +/Next 964 0 R >> endobj 964 0 obj << -/Title (PT) -/Dest [ 214 0 R /XYZ 40.316457 625.834422 0 ] -/Count 1 -/First 965 0 R -/Last 965 0 R -/Parent 963 0 R -/Next 966 0 R +/Title +/Dest [ 206 0 R /XYZ 37.466457 419.132022 0 ] +/Count 0 +/Prev 963 0 R +/Parent 962 0 R +/Next 965 0 R >> endobj 965 0 obj << -/Title -/Dest [ 214 0 R /XYZ 40.316457 585.524022 0 ] +/Title (1.2.3 Cas d'utilisation) +/Dest [ 213 0 R /XYZ 37.466457 142.669266 0 ] /Count 0 -/Parent 964 0 R +/Prev 964 0 R +/Parent 962 0 R +/Next 966 0 R >> endobj 966 0 obj << -/Title (DT) -/Dest [ 214 0 R /XYZ 40.316457 493.384662 0 ] -/Count 6 -/Prev 964 0 R -/First 967 0 R -/Last 972 0 R -/Parent 963 0 R -/Next 973 0 R +/Title +/Dest [ 218 0 R /XYZ 37.466457 479.548422 0 ] +/Count 0 +/Prev 965 0 R +/Parent 962 0 R >> endobj 967 0 obj << -/Title -/Dest [ 214 0 R /XYZ 40.316457 453.074262 0 ] +/Title +/Dest [ 218 0 R /XYZ 37.466457 293.549622 0 ] /Count 0 -/Parent 966 0 R +/Prev 962 0 R +/Parent 957 0 R /Next 968 0 R >> endobj 968 0 obj << -/Title (DT2 Documentation Analyse de l'existant \(2\)) -/Dest [ 214 0 R /XYZ 40.316457 370.282902 0 ] -/Count 0 +/Title +/Dest [ 218 0 R /XYZ 37.466457 219.482922 0 ] +/Count 19 /Prev 967 0 R -/Parent 966 0 R -/Next 969 0 R +/First 969 0 R +/Last 969 0 R +/Parent 957 0 R +/Next 988 0 R >> endobj 969 0 obj << -/Title (DT3 Documentation Analyse organique \(5\)) -/Dest [ 214 0 R /XYZ 40.316457 299.894742 0 ] -/Count 0 -/Prev 968 0 R -/Parent 966 0 R -/Next 970 0 R +/Title +/Dest [ 220 0 R /XYZ 37.466457 771.023622 0 ] +/Count 18 +/First 970 0 R +/Last 985 0 R +/Parent 968 0 R >> endobj 970 0 obj << -/Title (DT4 Documentation Analyse fonctionnelle \(2\)) -/Dest [ 214 0 R /XYZ 40.316457 196.948182 0 ] -/Count 0 -/Prev 969 0 R -/Parent 966 0 R -/Next 971 0 R +/Title (PT) +/Dest [ 220 0 R /XYZ 40.316457 625.834422 0 ] +/Count 1 +/First 971 0 R +/Last 971 0 R +/Parent 969 0 R +/Next 972 0 R >> endobj 971 0 obj << -/Title (DT5 Documentation Tests \(1\)) -/Dest [ 216 0 R /XYZ 40.316457 771.023622 0 ] +/Title +/Dest [ 220 0 R /XYZ 40.316457 585.524022 0 ] /Count 0 -/Prev 970 0 R -/Parent 966 0 R -/Next 972 0 R +/Parent 970 0 R >> endobj 972 0 obj << -/Title (DT6 Documentation Reste \(2\)) -/Dest [ 216 0 R /XYZ 40.316457 720.790662 0 ] -/Count 0 -/Prev 971 0 R -/Parent 966 0 R +/Title (DT) +/Dest [ 220 0 R /XYZ 40.316457 493.384662 0 ] +/Count 6 +/Prev 970 0 R +/First 973 0 R +/Last 978 0 R +/Parent 969 0 R +/Next 979 0 R >> endobj 973 0 obj << -/Title (PT) -/Dest [ 216 0 R /XYZ 40.316457 648.806502 0 ] -/Count 5 -/Prev 966 0 R -/First 974 0 R -/Last 978 0 R -/Parent 963 0 R -/Next 979 0 R +/Title +/Dest [ 220 0 R /XYZ 40.316457 453.074262 0 ] +/Count 0 +/Parent 972 0 R +/Next 974 0 R >> endobj 974 0 obj << -/Title -/Dest [ 216 0 R /XYZ 40.316457 608.496102 0 ] +/Title (DT2 Documentation Analyse de l'existant \(2\)) +/Dest [ 220 0 R /XYZ 40.316457 370.282902 0 ] /Count 0 -/Parent 973 0 R +/Prev 973 0 R +/Parent 972 0 R /Next 975 0 R >> endobj 975 0 obj << -/Title (PT2 Programmation OCR \(5\)) -/Dest [ 216 0 R /XYZ 40.316457 455.936742 0 ] +/Title (DT3 Documentation Analyse organique \(5\)) +/Dest [ 220 0 R /XYZ 40.316457 299.894742 0 ] /Count 0 /Prev 974 0 R -/Parent 973 0 R +/Parent 972 0 R /Next 976 0 R >> endobj 976 0 obj << -/Title -/Dest [ 216 0 R /XYZ 40.316457 360.742182 0 ] +/Title (DT4 Documentation Analyse fonctionnelle \(2\)) +/Dest [ 220 0 R /XYZ 40.316457 196.948182 0 ] /Count 0 /Prev 975 0 R -/Parent 973 0 R +/Parent 972 0 R /Next 977 0 R >> endobj 977 0 obj << -/Title (PT4 Programmation Vue de l'APP \(5\)) -/Dest [ 216 0 R /XYZ 40.316457 277.950822 0 ] +/Title (DT5 Documentation Tests \(1\)) +/Dest [ 222 0 R /XYZ 40.316457 771.023622 0 ] /Count 0 /Prev 976 0 R -/Parent 973 0 R +/Parent 972 0 R /Next 978 0 R >> endobj 978 0 obj << -/Title (PT5 Programmation mise en commun \(3\)) -/Dest [ 216 0 R /XYZ 40.316457 182.756262 0 ] +/Title (DT6 Documentation Reste \(2\)) +/Dest [ 222 0 R /XYZ 40.316457 720.790662 0 ] /Count 0 /Prev 977 0 R -/Parent 973 0 R +/Parent 972 0 R >> endobj 979 0 obj << -/Title (TT) -/Dest [ 218 0 R /XYZ 40.316457 771.023622 0 ] -/Count 2 -/Prev 973 0 R +/Title (PT) +/Dest [ 222 0 R /XYZ 40.316457 648.806502 0 ] +/Count 5 +/Prev 972 0 R /First 980 0 R -/Last 981 0 R -/Parent 963 0 R +/Last 984 0 R +/Parent 969 0 R +/Next 985 0 R >> endobj 980 0 obj << -/Title (TT1 Tests OCR \(2\)) -/Dest [ 218 0 R /XYZ 40.316457 718.310022 0 ] +/Title +/Dest [ 222 0 R /XYZ 40.316457 608.496102 0 ] /Count 0 /Parent 979 0 R /Next 981 0 R @@ -14301,60 +14420,58 @@ endobj endobj 981 0 obj << -/Title (TT2 Tests finaux \(2\)) -/Dest [ 218 0 R /XYZ 40.316457 578.153862 0 ] +/Title (PT2 Programmation OCR \(5\)) +/Dest [ 222 0 R /XYZ 40.316457 455.936742 0 ] /Count 0 /Prev 980 0 R /Parent 979 0 R +/Next 982 0 R >> endobj 982 0 obj << -/Title -/Dest [ 218 0 R /XYZ 37.466457 458.928102 0 ] +/Title +/Dest [ 222 0 R /XYZ 40.316457 360.742182 0 ] /Count 0 -/Prev 962 0 R -/Parent 951 0 R +/Prev 981 0 R +/Parent 979 0 R /Next 983 0 R >> endobj 983 0 obj << -/Title (1.6 Analyse fonctionnelle) -/Dest [ 218 0 R /XYZ 37.466457 384.861402 0 ] +/Title (PT4 Programmation Vue de l'APP \(5\)) +/Dest [ 222 0 R /XYZ 40.316457 277.950822 0 ] /Count 0 /Prev 982 0 R -/Parent 951 0 R +/Parent 979 0 R /Next 984 0 R >> endobj 984 0 obj << -/Title (1.7 Analyse Organique) -/Dest [ 218 0 R /XYZ 37.466457 310.794702 0 ] -/Count 18 +/Title (PT5 Programmation mise en commun \(3\)) +/Dest [ 222 0 R /XYZ 40.316457 182.756262 0 ] +/Count 0 /Prev 983 0 R -/First 985 0 R -/Last 1002 0 R -/Parent 951 0 R -/Next 1003 0 R +/Parent 979 0 R >> endobj 985 0 obj << -/Title -/Dest [ 218 0 R /XYZ 37.466457 240.604002 0 ] -/Count 5 +/Title (TT) +/Dest [ 224 0 R /XYZ 40.316457 771.023622 0 ] +/Count 2 +/Prev 979 0 R /First 986 0 R -/Last 990 0 R -/Parent 984 0 R -/Next 991 0 R +/Last 987 0 R +/Parent 969 0 R >> endobj 986 0 obj << -/Title (Comment faire ?) -/Dest [ 222 0 R /XYZ 40.316457 208.515666 0 ] +/Title (TT1 Tests OCR \(2\)) +/Dest [ 224 0 R /XYZ 40.316457 718.310022 0 ] /Count 0 /Parent 985 0 R /Next 987 0 R @@ -14362,781 +14479,872 @@ endobj endobj 987 0 obj << -/Title (Simuler un navigateur ?) -/Dest [ 227 0 R /XYZ 40.316457 451.595622 0 ] +/Title (TT2 Tests finaux \(2\)) +/Dest [ 224 0 R /XYZ 40.316457 578.153862 0 ] /Count 0 /Prev 986 0 R /Parent 985 0 R -/Next 988 0 R >> endobj 988 0 obj << -/Title -/Dest [ 232 0 R /XYZ 40.316457 538.688022 0 ] +/Title +/Dest [ 224 0 R /XYZ 37.466457 458.928102 0 ] /Count 0 -/Prev 987 0 R -/Parent 985 0 R +/Prev 968 0 R +/Parent 957 0 R /Next 989 0 R >> endobj 989 0 obj << -/Title -/Dest [ 237 0 R /XYZ 40.316457 419.812422 0 ] +/Title (1.6 Analyse fonctionnelle) +/Dest [ 224 0 R /XYZ 37.466457 384.861402 0 ] /Count 0 /Prev 988 0 R -/Parent 985 0 R +/Parent 957 0 R /Next 990 0 R >> endobj 990 0 obj << -/Title (Calibration) -/Dest [ 237 0 R /XYZ 40.316457 337.595622 0 ] -/Count 0 +/Title (1.7 Analyse Organique) +/Dest [ 224 0 R /XYZ 37.466457 310.794702 0 ] +/Count 18 /Prev 989 0 R -/Parent 985 0 R +/First 991 0 R +/Last 1008 0 R +/Parent 957 0 R +/Next 1009 0 R >> endobj 991 0 obj << -/Title (1.7.2 OCR) -/Dest [ 237 0 R /XYZ 37.466457 289.533222 0 ] -/Count 7 -/Prev 985 0 R +/Title +/Dest [ 224 0 R /XYZ 37.466457 240.604002 0 ] +/Count 5 /First 992 0 R -/Last 992 0 R -/Parent 984 0 R -/Next 999 0 R +/Last 996 0 R +/Parent 990 0 R +/Next 997 0 R >> endobj 992 0 obj << -/Title -/Dest [ 237 0 R /XYZ 40.316457 131.940822 0 ] -/Count 6 -/First 993 0 R -/Last 993 0 R +/Title (Comment faire ?) +/Dest [ 228 0 R /XYZ 40.316457 208.515666 0 ] +/Count 0 /Parent 991 0 R +/Next 993 0 R >> endobj 993 0 obj << -/Title (Filtres et traitement) -/Dest [ 276 0 R /XYZ 40.316457 465.594822 0 ] -/Count 5 -/First 994 0 R -/Last 998 0 R -/Parent 992 0 R +/Title (Simuler un navigateur ?) +/Dest [ 233 0 R /XYZ 40.316457 451.595622 0 ] +/Count 0 +/Prev 992 0 R +/Parent 991 0 R +/Next 994 0 R >> endobj 994 0 obj << -/Title (Texte) -/Dest [ 297 0 R /XYZ 40.316457 771.023622 0 ] +/Title +/Dest [ 238 0 R /XYZ 40.316457 538.688022 0 ] /Count 0 -/Parent 993 0 R +/Prev 993 0 R +/Parent 991 0 R /Next 995 0 R >> endobj 995 0 obj << -/Title (Chiffres) -/Dest [ 312 0 R /XYZ 40.316457 384.462822 0 ] +/Title +/Dest [ 243 0 R /XYZ 40.316457 419.812422 0 ] /Count 0 /Prev 994 0 R -/Parent 993 0 R +/Parent 991 0 R /Next 996 0 R >> endobj 996 0 obj << -/Title (Pneus) -/Dest [ 331 0 R /XYZ 40.316457 221.934822 0 ] +/Title (Calibration) +/Dest [ 243 0 R /XYZ 40.316457 337.595622 0 ] /Count 0 /Prev 995 0 R -/Parent 993 0 R -/Next 997 0 R +/Parent 991 0 R >> endobj 997 0 obj << -/Title (DRS) -/Dest [ 395 0 R /XYZ 40.316457 322.168422 0 ] -/Count 0 -/Prev 996 0 R -/Parent 993 0 R -/Next 998 0 R +/Title (1.7.2 OCR) +/Dest [ 243 0 R /XYZ 37.466457 289.533222 0 ] +/Count 7 +/Prev 991 0 R +/First 998 0 R +/Last 998 0 R +/Parent 990 0 R +/Next 1005 0 R >> endobj 998 0 obj << -/Title (Filtres et methodes sur les images) -/Dest [ 395 0 R /XYZ 40.316457 284.338662 0 ] -/Count 0 -/Prev 997 0 R -/Parent 993 0 R +/Title +/Dest [ 243 0 R /XYZ 40.316457 131.940822 0 ] +/Count 6 +/First 999 0 R +/Last 999 0 R +/Parent 997 0 R >> endobj 999 0 obj << -/Title -/Dest [ 414 0 R /XYZ 37.466457 771.023622 0 ] -/Count 0 -/Prev 991 0 R -/Parent 984 0 R -/Next 1000 0 R +/Title (Filtres et traitement) +/Dest [ 282 0 R /XYZ 40.316457 465.594822 0 ] +/Count 5 +/First 1000 0 R +/Last 1004 0 R +/Parent 998 0 R >> endobj 1000 0 obj << -/Title -/Dest [ 414 0 R /XYZ 37.466457 740.609622 0 ] +/Title (Texte) +/Dest [ 303 0 R /XYZ 40.316457 771.023622 0 ] /Count 0 -/Prev 999 0 R -/Parent 984 0 R +/Parent 999 0 R /Next 1001 0 R >> endobj 1001 0 obj << -/Title -/Dest [ 414 0 R /XYZ 37.466457 710.195622 0 ] +/Title (Chiffres) +/Dest [ 318 0 R /XYZ 40.316457 384.462822 0 ] /Count 0 /Prev 1000 0 R -/Parent 984 0 R +/Parent 999 0 R /Next 1002 0 R >> endobj 1002 0 obj << -/Title -/Dest [ 414 0 R /XYZ 37.466457 679.781622 0 ] +/Title (Pneus) +/Dest [ 337 0 R /XYZ 40.316457 221.934822 0 ] /Count 0 /Prev 1001 0 R -/Parent 984 0 R +/Parent 999 0 R +/Next 1003 0 R >> endobj 1003 0 obj << -/Title (1.8 Tests) -/Dest [ 414 0 R /XYZ 37.466457 645.491622 0 ] +/Title (DRS) +/Dest [ 401 0 R /XYZ 40.316457 322.168422 0 ] /Count 0 -/Prev 984 0 R -/Parent 951 0 R +/Prev 1002 0 R +/Parent 999 0 R /Next 1004 0 R >> endobj 1004 0 obj << -/Title -/Dest [ 414 0 R /XYZ 37.466457 571.424922 0 ] +/Title (Filtres et methodes sur les images) +/Dest [ 401 0 R /XYZ 40.316457 284.338662 0 ] /Count 0 /Prev 1003 0 R -/Parent 951 0 R -/Next 1005 0 R +/Parent 999 0 R >> endobj 1005 0 obj << -/Title -/Dest [ 414 0 R /XYZ 37.466457 497.358222 0 ] +/Title +/Dest [ 420 0 R /XYZ 37.466457 771.023622 0 ] /Count 0 -/Prev 1004 0 R -/Parent 951 0 R +/Prev 997 0 R +/Parent 990 0 R /Next 1006 0 R >> endobj 1006 0 obj << -/Title (1.11 Conclusion) -/Dest [ 414 0 R /XYZ 37.466457 423.291522 0 ] +/Title +/Dest [ 420 0 R /XYZ 37.466457 740.609622 0 ] /Count 0 /Prev 1005 0 R -/Parent 951 0 R +/Parent 990 0 R +/Next 1007 0 R >> endobj 1007 0 obj << -/Title (2. Cahier des charges) -/Dest [ 416 0 R /XYZ 37.466457 759.623622 0 ] -/Count 5 -/Prev 951 0 R -/First 1008 0 R -/Last 1012 0 R -/Next 1013 0 R -/Parent 1053 0 R +/Title +/Dest [ 420 0 R /XYZ 37.466457 710.195622 0 ] +/Count 0 +/Prev 1006 0 R +/Parent 990 0 R +/Next 1008 0 R >> endobj 1008 0 obj << -/Title (2.1 Contexte) -/Dest [ 416 0 R /XYZ 37.466457 686.805222 0 ] +/Title +/Dest [ 420 0 R /XYZ 37.466457 679.781622 0 ] /Count 0 -/Parent 1007 0 R -/Next 1009 0 R +/Prev 1007 0 R +/Parent 990 0 R >> endobj 1009 0 obj << -/Title (2.2 Projet) -/Dest [ 423 0 R /XYZ 37.466457 592.727622 0 ] +/Title (1.8 Tests) +/Dest [ 420 0 R /XYZ 37.466457 645.491622 0 ] /Count 0 -/Prev 1008 0 R -/Parent 1007 0 R +/Prev 990 0 R +/Parent 957 0 R /Next 1010 0 R >> endobj 1010 0 obj << -/Title -/Dest [ 423 0 R /XYZ 37.466457 286.876122 0 ] +/Title +/Dest [ 420 0 R /XYZ 37.466457 571.424922 0 ] /Count 0 /Prev 1009 0 R -/Parent 1007 0 R +/Parent 957 0 R /Next 1011 0 R >> endobj 1011 0 obj << -/Title (2.4 Cas d'utilisation) -/Dest [ 430 0 R /XYZ 37.466457 138.793266 0 ] +/Title (1.10 Optimisation du programme) +/Dest [ 420 0 R /XYZ 37.466457 497.358222 0 ] /Count 0 /Prev 1010 0 R -/Parent 1007 0 R +/Parent 957 0 R /Next 1012 0 R >> endobj 1012 0 obj << -/Title -/Dest [ 435 0 R /XYZ 37.466457 475.672422 0 ] +/Title (1.11 Ethique du projet) +/Dest [ 420 0 R /XYZ 37.466457 423.291522 0 ] /Count 0 /Prev 1011 0 R -/Parent 1007 0 R +/Parent 957 0 R +/Next 1013 0 R >> endobj 1013 0 obj << -/Title (3. Journal de bord) -/Dest [ 437 0 R /XYZ 37.466457 759.623622 0 ] -/Count 20 -/Prev 1007 0 R -/First 1014 0 R -/Last 1033 0 R -/Next 1034 0 R -/Parent 1053 0 R +/Title +/Dest [ 420 0 R /XYZ 37.466457 336.821622 0 ] +/Count 0 +/Prev 1012 0 R +/Parent 957 0 R +/Next 1014 0 R >> endobj 1014 0 obj << -/Title (3.1 Mercredi 29 Mars 2023) -/Dest [ 437 0 R /XYZ 37.466457 718.588422 0 ] +/Title (1.13 Conclusion) +/Dest [ 420 0 R /XYZ 37.466457 262.754922 0 ] /Count 0 -/Parent 1013 0 R -/Next 1015 0 R +/Prev 1013 0 R +/Parent 957 0 R >> endobj 1015 0 obj << -/Title (3.2 Jeudi 30 Mars 2023) -/Dest [ 439 0 R /XYZ 37.466457 330.643313 0 ] -/Count 0 -/Prev 1014 0 R -/Parent 1013 0 R -/Next 1016 0 R +/Title (2. Cahier des charges) +/Dest [ 422 0 R /XYZ 37.466457 759.623622 0 ] +/Count 5 +/Prev 957 0 R +/First 1016 0 R +/Last 1020 0 R +/Next 1021 0 R +/Parent 1061 0 R >> endobj 1016 0 obj << -/Title (3.3 Vendredi 31/03/2023) -/Dest [ 450 0 R /XYZ 37.466457 458.618022 0 ] +/Title (2.1 Contexte) +/Dest [ 422 0 R /XYZ 37.466457 686.805222 0 ] /Count 0 -/Prev 1015 0 R -/Parent 1013 0 R +/Parent 1015 0 R /Next 1017 0 R >> endobj 1017 0 obj << -/Title (3.4 Lundi 3 Avril) -/Dest [ 521 0 R /XYZ 37.466457 771.023622 0 ] +/Title (2.2 Projet) +/Dest [ 429 0 R /XYZ 37.466457 592.727622 0 ] /Count 0 /Prev 1016 0 R -/Parent 1013 0 R +/Parent 1015 0 R /Next 1018 0 R >> endobj 1018 0 obj << -/Title (3.5 Mardi 4 Avril) -/Dest [ 586 0 R /XYZ 37.466457 694.278822 0 ] +/Title +/Dest [ 429 0 R /XYZ 37.466457 286.876122 0 ] /Count 0 /Prev 1017 0 R -/Parent 1013 0 R +/Parent 1015 0 R /Next 1019 0 R >> endobj 1019 0 obj << -/Title (3.6 Mercredi 5 Avril) -/Dest [ 606 0 R /XYZ 37.466457 125.723886 0 ] +/Title (2.4 Cas d'utilisation) +/Dest [ 436 0 R /XYZ 37.466457 138.793266 0 ] /Count 0 /Prev 1018 0 R -/Parent 1013 0 R +/Parent 1015 0 R /Next 1020 0 R >> endobj 1020 0 obj << -/Title (3.7 Jeudi 6 Avril) -/Dest [ 631 0 R /XYZ 37.466457 250.585350 0 ] +/Title +/Dest [ 441 0 R /XYZ 37.466457 475.672422 0 ] /Count 0 /Prev 1019 0 R -/Parent 1013 0 R -/Next 1021 0 R +/Parent 1015 0 R >> endobj 1021 0 obj << -/Title (3.8 Vendredi 6 Avril 2023) -/Dest [ 672 0 R /XYZ 37.466457 415.188330 0 ] -/Count 0 -/Prev 1020 0 R -/Parent 1013 0 R -/Next 1022 0 R +/Title (3. Journal de bord) +/Dest [ 443 0 R /XYZ 37.466457 759.623622 0 ] +/Count 20 +/Prev 1015 0 R +/First 1022 0 R +/Last 1041 0 R +/Next 1042 0 R +/Parent 1061 0 R >> endobj 1022 0 obj << -/Title (3.9 Vacances) -/Dest [ 697 0 R /XYZ 37.466457 389.906022 0 ] +/Title (3.1 Mercredi 29 Mars 2023) +/Dest [ 443 0 R /XYZ 37.466457 718.588422 0 ] /Count 0 -/Prev 1021 0 R -/Parent 1013 0 R +/Parent 1021 0 R /Next 1023 0 R >> endobj 1023 0 obj << -/Title (3.10 Lundi 24 Avril 2023) -/Dest [ 745 0 R /XYZ 37.466457 386.895102 0 ] +/Title (3.2 Jeudi 30 Mars 2023) +/Dest [ 445 0 R /XYZ 37.466457 330.643313 0 ] /Count 0 /Prev 1022 0 R -/Parent 1013 0 R +/Parent 1021 0 R /Next 1024 0 R >> endobj 1024 0 obj << -/Title (3.11 Mardi 25 Avril 2023) -/Dest [ 753 0 R /XYZ 37.466457 205.106022 0 ] +/Title (3.3 Vendredi 31/03/2023) +/Dest [ 456 0 R /XYZ 37.466457 458.618022 0 ] /Count 0 /Prev 1023 0 R -/Parent 1013 0 R +/Parent 1021 0 R /Next 1025 0 R >> endobj 1025 0 obj << -/Title (3.12 26 Avril 2023) -/Dest [ 763 0 R /XYZ 37.466457 559.394022 0 ] +/Title (3.4 Lundi 3 Avril) +/Dest [ 527 0 R /XYZ 37.466457 771.023622 0 ] /Count 0 /Prev 1024 0 R -/Parent 1013 0 R +/Parent 1021 0 R /Next 1026 0 R >> endobj 1026 0 obj << -/Title (3.13 Jeudi 27 Avril 2023) -/Dest [ 766 0 R /XYZ 37.466457 317.480782 0 ] +/Title (3.5 Mardi 4 Avril) +/Dest [ 592 0 R /XYZ 37.466457 694.278822 0 ] /Count 0 /Prev 1025 0 R -/Parent 1013 0 R +/Parent 1021 0 R /Next 1027 0 R >> endobj 1027 0 obj << -/Title (3.14 Vendredi 28 Avril 2023) -/Dest [ 780 0 R /XYZ 37.466457 771.023622 0 ] +/Title (3.6 Mercredi 5 Avril) +/Dest [ 612 0 R /XYZ 37.466457 125.723886 0 ] /Count 0 /Prev 1026 0 R -/Parent 1013 0 R +/Parent 1021 0 R /Next 1028 0 R >> endobj 1028 0 obj << -/Title (3.15 Lundi 1 Mai 2023) -/Dest [ 780 0 R /XYZ 37.466457 137.264682 0 ] +/Title (3.7 Jeudi 6 Avril) +/Dest [ 637 0 R /XYZ 37.466457 250.585350 0 ] /Count 0 /Prev 1027 0 R -/Parent 1013 0 R +/Parent 1021 0 R /Next 1029 0 R >> endobj 1029 0 obj << -/Title (3.16 Mardi 2 Mai 2023) -/Dest [ 784 0 R /XYZ 37.466457 186.104214 0 ] +/Title (3.8 Vendredi 6 Avril 2023) +/Dest [ 678 0 R /XYZ 37.466457 415.188330 0 ] /Count 0 /Prev 1028 0 R -/Parent 1013 0 R +/Parent 1021 0 R /Next 1030 0 R >> endobj 1030 0 obj << -/Title (3.17 Recrutement Payerne Mai 2023) -/Dest [ 806 0 R /XYZ 37.466457 653.968422 0 ] +/Title (3.9 Vacances) +/Dest [ 703 0 R /XYZ 37.466457 389.906022 0 ] /Count 0 /Prev 1029 0 R -/Parent 1013 0 R +/Parent 1021 0 R /Next 1031 0 R >> endobj 1031 0 obj << -/Title (3.18 Vendredi 5 Mai 2023) -/Dest [ 806 0 R /XYZ 37.466457 571.919322 0 ] +/Title (3.10 Lundi 24 Avril 2023) +/Dest [ 751 0 R /XYZ 37.466457 386.895102 0 ] /Count 0 /Prev 1030 0 R -/Parent 1013 0 R +/Parent 1021 0 R /Next 1032 0 R >> endobj 1032 0 obj << -/Title (3.19 Lundi 8 Mai 2023) -/Dest [ 811 0 R /XYZ 37.466457 686.526822 0 ] +/Title (3.11 Mardi 25 Avril 2023) +/Dest [ 759 0 R /XYZ 37.466457 205.106022 0 ] /Count 0 /Prev 1031 0 R -/Parent 1013 0 R +/Parent 1021 0 R /Next 1033 0 R >> endobj 1033 0 obj << -/Title (3.20 Mardi 9 Mai 2023) -/Dest [ 845 0 R /XYZ 37.466457 226.356188 0 ] +/Title (3.12 26 Avril 2023) +/Dest [ 769 0 R /XYZ 37.466457 559.394022 0 ] /Count 0 /Prev 1032 0 R -/Parent 1013 0 R +/Parent 1021 0 R +/Next 1034 0 R >> endobj 1034 0 obj << -/Title (4. Code) -/Dest [ 867 0 R /XYZ 37.466457 759.623622 0 ] -/Count 18 -/Prev 1013 0 R -/First 1035 0 R -/Last 1052 0 R -/Parent 1053 0 R +/Title (3.13 Jeudi 27 Avril 2023) +/Dest [ 772 0 R /XYZ 37.466457 317.480782 0 ] +/Count 0 +/Prev 1033 0 R +/Parent 1021 0 R +/Next 1035 0 R >> endobj 1035 0 obj << -/Title (4.1 ConfigurationTool.cs) -/Dest [ 867 0 R /XYZ 37.466457 718.588422 0 ] +/Title (3.14 Vendredi 28 Avril 2023) +/Dest [ 786 0 R /XYZ 37.466457 771.023622 0 ] /Count 0 -/Parent 1034 0 R +/Prev 1034 0 R +/Parent 1021 0 R /Next 1036 0 R >> endobj 1036 0 obj << -/Title (4.2 DriverGapToLeaderWindow.cs) -/Dest [ 873 0 R /XYZ 37.466457 751.643622 0 ] +/Title (3.15 Lundi 1 Mai 2023) +/Dest [ 786 0 R /XYZ 37.466457 137.264682 0 ] /Count 0 /Prev 1035 0 R -/Parent 1034 0 R +/Parent 1021 0 R /Next 1037 0 R >> endobj 1037 0 obj << -/Title (4.3 DriverPositionWindow.cs) -/Dest [ 875 0 R /XYZ 37.466457 751.643622 0 ] +/Title (3.16 Mardi 2 Mai 2023) +/Dest [ 790 0 R /XYZ 37.466457 186.104214 0 ] /Count 0 /Prev 1036 0 R -/Parent 1034 0 R +/Parent 1021 0 R /Next 1038 0 R >> endobj 1038 0 obj << -/Title (4.4 F1TVEmulator.cs) -/Dest [ 877 0 R /XYZ 37.466457 751.643622 0 ] +/Title (3.17 Recrutement Payerne Mai 2023) +/Dest [ 812 0 R /XYZ 37.466457 653.968422 0 ] /Count 0 /Prev 1037 0 R -/Parent 1034 0 R +/Parent 1021 0 R /Next 1039 0 R >> endobj 1039 0 obj << -/Title (4.5 Program.cs) -/Dest [ 885 0 R /XYZ 37.466457 751.643622 0 ] +/Title (3.18 Vendredi 5 Mai 2023) +/Dest [ 812 0 R /XYZ 37.466457 571.919322 0 ] /Count 0 /Prev 1038 0 R -/Parent 1034 0 R +/Parent 1021 0 R /Next 1040 0 R >> endobj 1040 0 obj << -/Title (4.6 Window.cs) -/Dest [ 887 0 R /XYZ 37.466457 751.643622 0 ] +/Title (3.19 Lundi 8 Mai 2023) +/Dest [ 817 0 R /XYZ 37.466457 686.526822 0 ] /Count 0 /Prev 1039 0 R -/Parent 1034 0 R +/Parent 1021 0 R /Next 1041 0 R >> endobj 1041 0 obj << -/Title (4.7 DriverData.cs) -/Dest [ 895 0 R /XYZ 37.466457 751.643622 0 ] +/Title (3.20 Mardi 9 Mai 2023) +/Dest [ 851 0 R /XYZ 37.466457 226.356188 0 ] /Count 0 /Prev 1040 0 R -/Parent 1034 0 R -/Next 1042 0 R +/Parent 1021 0 R >> endobj 1042 0 obj << -/Title (4.8 DriverLapTimeWindow.cs) -/Dest [ 899 0 R /XYZ 37.466457 751.643622 0 ] -/Count 0 -/Prev 1041 0 R -/Parent 1034 0 R -/Next 1043 0 R +/Title (4. Code) +/Dest [ 873 0 R /XYZ 37.466457 759.623622 0 ] +/Count 18 +/Prev 1021 0 R +/First 1043 0 R +/Last 1060 0 R +/Parent 1061 0 R >> endobj 1043 0 obj << -/Title (4.9 DriverSectorWindow.cs) -/Dest [ 901 0 R /XYZ 37.466457 751.643622 0 ] +/Title (4.1 ConfigurationTool.cs) +/Dest [ 873 0 R /XYZ 37.466457 718.588422 0 ] /Count 0 -/Prev 1042 0 R -/Parent 1034 0 R +/Parent 1042 0 R /Next 1044 0 R >> endobj 1044 0 obj << -/Title (4.10 Form1.cs) -/Dest [ 903 0 R /XYZ 37.466457 751.643622 0 ] +/Title (4.2 DriverGapToLeaderWindow.cs) +/Dest [ 879 0 R /XYZ 37.466457 751.643622 0 ] /Count 0 /Prev 1043 0 R -/Parent 1034 0 R +/Parent 1042 0 R /Next 1045 0 R >> endobj 1045 0 obj << -/Title (4.11 Reader.cs) -/Dest [ 905 0 R /XYZ 37.466457 751.643622 0 ] +/Title (4.3 DriverPositionWindow.cs) +/Dest [ 881 0 R /XYZ 37.466457 751.643622 0 ] /Count 0 /Prev 1044 0 R -/Parent 1034 0 R +/Parent 1042 0 R /Next 1046 0 R >> endobj 1046 0 obj << -/Title (4.12 Zone.cs) -/Dest [ 911 0 R /XYZ 37.466457 751.643622 0 ] +/Title (4.4 F1TVEmulator.cs) +/Dest [ 883 0 R /XYZ 37.466457 751.643622 0 ] /Count 0 /Prev 1045 0 R -/Parent 1034 0 R +/Parent 1042 0 R /Next 1047 0 R >> endobj 1047 0 obj << -/Title (4.13 DriverDrsWindow.cs) -/Dest [ 917 0 R /XYZ 37.466457 751.643622 0 ] +/Title (4.5 Program.cs) +/Dest [ 891 0 R /XYZ 37.466457 751.643622 0 ] /Count 0 /Prev 1046 0 R -/Parent 1034 0 R +/Parent 1042 0 R /Next 1048 0 R >> endobj 1048 0 obj << -/Title (4.14 DriverNameWindow.cs) -/Dest [ 921 0 R /XYZ 37.466457 751.643622 0 ] +/Title (4.6 Window.cs) +/Dest [ 893 0 R /XYZ 37.466457 751.643622 0 ] /Count 0 /Prev 1047 0 R -/Parent 1034 0 R +/Parent 1042 0 R /Next 1049 0 R >> endobj 1049 0 obj << -/Title (4.15 DriverTyresWindow.cs) -/Dest [ 923 0 R /XYZ 37.466457 751.643622 0 ] +/Title (4.7 DriverData.cs) +/Dest [ 901 0 R /XYZ 37.466457 751.643622 0 ] /Count 0 /Prev 1048 0 R -/Parent 1034 0 R +/Parent 1042 0 R /Next 1050 0 R >> endobj 1050 0 obj << -/Title (4.16 OcrImage.cs) -/Dest [ 927 0 R /XYZ 37.466457 751.643622 0 ] +/Title (4.8 DriverLapTimeWindow.cs) +/Dest [ 905 0 R /XYZ 37.466457 751.643622 0 ] /Count 0 /Prev 1049 0 R -/Parent 1034 0 R +/Parent 1042 0 R /Next 1051 0 R >> endobj 1051 0 obj << -/Title (4.17 Settings.cs) -/Dest [ 939 0 R /XYZ 37.466457 751.643622 0 ] +/Title (4.9 DriverSectorWindow.cs) +/Dest [ 907 0 R /XYZ 37.466457 751.643622 0 ] /Count 0 /Prev 1050 0 R -/Parent 1034 0 R +/Parent 1042 0 R /Next 1052 0 R >> endobj 1052 0 obj << -/Title (4.18 recoverCookiesCSV.py) -/Dest [ 949 0 R /XYZ 37.466457 751.643622 0 ] +/Title (4.10 Form1.cs) +/Dest [ 909 0 R /XYZ 37.466457 751.643622 0 ] /Count 0 /Prev 1051 0 R -/Parent 1034 0 R +/Parent 1042 0 R +/Next 1053 0 R >> endobj 1053 0 obj << -/Count 103 -/First 950 0 R -/Last 1034 0 R +/Title (4.11 Reader.cs) +/Dest [ 911 0 R /XYZ 37.466457 751.643622 0 ] +/Count 0 +/Prev 1052 0 R +/Parent 1042 0 R +/Next 1054 0 R >> endobj 1054 0 obj << +/Title (4.12 Zone.cs) +/Dest [ 917 0 R /XYZ 37.466457 751.643622 0 ] +/Count 0 +/Prev 1053 0 R +/Parent 1042 0 R +/Next 1055 0 R +>> +endobj +1055 0 obj +<< +/Title (4.13 DriverDrsWindow.cs) +/Dest [ 923 0 R /XYZ 37.466457 751.643622 0 ] +/Count 0 +/Prev 1054 0 R +/Parent 1042 0 R +/Next 1056 0 R +>> +endobj +1056 0 obj +<< +/Title (4.14 DriverNameWindow.cs) +/Dest [ 927 0 R /XYZ 37.466457 751.643622 0 ] +/Count 0 +/Prev 1055 0 R +/Parent 1042 0 R +/Next 1057 0 R +>> +endobj +1057 0 obj +<< +/Title (4.15 DriverTyresWindow.cs) +/Dest [ 929 0 R /XYZ 37.466457 751.643622 0 ] +/Count 0 +/Prev 1056 0 R +/Parent 1042 0 R +/Next 1058 0 R +>> +endobj +1058 0 obj +<< +/Title (4.16 OcrImage.cs) +/Dest [ 933 0 R /XYZ 37.466457 751.643622 0 ] +/Count 0 +/Prev 1057 0 R +/Parent 1042 0 R +/Next 1059 0 R +>> +endobj +1059 0 obj +<< +/Title (4.17 Settings.cs) +/Dest [ 945 0 R /XYZ 37.466457 751.643622 0 ] +/Count 0 +/Prev 1058 0 R +/Parent 1042 0 R +/Next 1060 0 R +>> +endobj +1060 0 obj +<< +/Title (4.18 recoverCookiesCSV.py) +/Dest [ 955 0 R /XYZ 37.466457 751.643622 0 ] +/Count 0 +/Prev 1059 0 R +/Parent 1042 0 R +>> +endobj +1061 0 obj +<< +/Count 105 +/First 956 0 R +/Last 1042 0 R +>> +endobj +1062 0 obj +<< /Length1 6744 /Filter /FlateDecode /Length 4267 >> stream -x9 XU{3\p@/ k#\P T@3MqDM - CjiG699PDb`8jrϾ^I_sY{Z{MBҌ'1` TC6ldF-HD3bt>NB،4DGN%3g q2rIb|l\St:'Olcu|LpВ:5\"wE؅i>q{LﶲsBZm@X۴ԌL  z6(N_o;yGnr':JYX3Sm[Qz iz| ψ)d;S8NQ Kx%bذݓܑ #drQsbT K\.`^0GI=A7FvUmz)>{?ؓo SBt3FYO! 2IgY>,Q{Z{FKq o)r4wj{H8 Tџ $W#9$#349T7e\NS6F5bS\[JJۋ=%η⿃m.G 1=ԓ֊U/Y7I9hyF&Z`렛[ۤۍ_nWrSߩp4pkB,Ԅy'-=!n2(5͹+ocimH_7o NcYHں`Wy=RS:S%5z؅wVXU,cibauɰYYN˛.uf - -6dywO8 ?'T,o s+7WVsNHT>&(%ojVBcډkOb c slT 1$TN3e&wyOӖOiWpw/,q/-ֲ"NN @!4EN)^>f!x{ò”IFܳyvV JY7 cSbNJt6w/=#g?(ey5ф}zE@EvoĪG}ܨ%.fR IuoZwSܺ"Iqw(N*ZIs4U1^^z[b;}FL57;>fLxѸV=ԣO"(5Aةފch[p -;Ve -p`4j9} -_dxНx(~܈j !aپWm[/.ZreMb-95{ݺr^eMJݙy>0? MyeWF:AωC^CwKر}KwӝI_nq5ҩU9ϜJGñwẺ[ -ߪh?:ͩJoMAپ7|V!lS{G^OIuI̽4x)%ج>Noίki~4,^nn;c7AH3w˽Ox5Aj5ZPGq5~UjV::RqL9q77|&lf˲r{i8'z> _Ke6zh h+~ }VODVBć]whsg5+/JZT|MVA{_~`*fOe(64Sn%&/Z4JȹAhy|?Ѭ[XH ;w}!MYXqZ Yiӑoա 'Gk_1wwxDg)ٜޜe@mDUB+G - dh,尼+?%eW?J%)++yz&5e]\|NEo}T&d7nicC{?Pzn$i*VJiv9Q[l׮ώo! `w+ QM43jՒWy㎇R{{ F:p4⫛Kq57H7{ˠ3[{3Ҭ jUS;&t;7Пy=W!OSWS20.ueo%hi$1HFL+͓Dl*-$ߢH -J:?B,Ѹ*C{g<{bSgbnA0Fhm]K]8*Qck;5m?m?4Po mOqsC_HGw".a(ͫK2H4S27x? Er 7v;ߑ +x9 XU{3\p@/ k#\P D@3MqDM + CjIG%699PDb`8j_+{?}>kZk\ByD  ;HՄs䂍@O2=&U+$ IO%T-Sx!-'6u ùo$ y0=#p߷"L9;]vqzLV*y@pݓcǹ܅V65%=:ye??b_Hk¯C\u)z"#v:5;ޱyǯ: RG:') b1HBI=d*P_‡ G]0B +)M>'I%ڜ FI_zғ}#.ldWѨb={|z҉?D=(u8d@J'}F5ڼ#Źw/FggjHx TLAd8$D\}& 98O9(bu!=v҄gbWz^.q\n}xp- p7cEYKҗ'$KI*$YJx3i4PLƐDb^֋֐ jU wjA8jmW !V FZ'8 $X8)=01[V$VKR")OˁFSUvZ[bflgrД?m=536;˥BM8zby&S·RS{VKjrE:l}[X0;َ`Ak{Vn{cJugsKj +tuvv1 ._$lzǒEY&k`x#I[ۋs'[=hPz v#)Xfh5VE7O+*ȝJ1:"}LPxEKfo?AaUF%՜; DDJS fO٨uw5d(bdP3arrU?=H]>i_icR^N^3_Zίa8~ZbO9MW,zzx K %ʪr-Sr٥gY (9gZ+I39:2ө>ɷ$kO>,ƣ@ED5zbNسy3RkN_/ q^Wٗk!RNDCp};ԺbT==||L#Qt(+>xʂ/qf{~C/ֱÁ]YQynt܊ Q3w*',~ۇ^35#?sf)2 =wm`Z W%Vey&\à"Ǭܴjw܈j a+aWಣ[//jMR 9${ݾbnEurٹ>0? MyeWF:AωC^@w+K]I%_~mqoC*/q95Vc -j*|N,6N*6W +GۊY]By ?Z+Qԉ;!2jҔ.>@bV$;}/5Ѱ4kFضekI{I ASB߭kd\0ҵz-` mcY9Ƴ#Y + +,tS[G#IRj솊G:ꈫ[9 u9G蹎wOp'mةlַ,-aO?B V4\jæWE$$Vp/'!=.>[z(G'/ٖ~\1gh&S Ԙ03uуuҒg5V}{ ] =) FFu[|!swŒo_M\C."sL?tS%ʈ݁U ȪĠ7VB??2W}~41j*ACz;}Wtَ_r)YYVG$[y(Rhο%\`@ X%Cc,^--)|UP2,I2魆eA)P魏 +٬?$F-8mv\nb P͚$n=Rɪ.3/9F]SiJg3A.(J`aIvSY_zW"?w]ص!E iz`8|e2̣1Yq[ fgݝn>pd3.%Y6vHh;>/uձuԱ;cN2/糉jxeOwJwܿq츼a4+W/~xhx94ϨWK^Ir9;JM'ݶR*V#j֩W7翗jn.%BgjgYV"6L^k٫0o?+;x(P@{|8DqL`.EDMLXfᬃdyOuo4[%V[]I}x; 5V p-ɀ;(ҕp|iUIG_DMߔmcH1>(4X + >KI2·PT$#_z\wҕtNq6o$L!q!|o\3ie=13` q{#t..ȁ~j(6:fv(귅6v'97BM#;G0%$_) [V;oZ endstream endobj -1055 0 obj +1063 0 obj << /Length1 32088 /Filter /FlateDecode -/Length 8367 +/Length 8366 >> stream -x} XTDzpwsfakԨ1Ơ(QW*(D("Q4j$3&14. obs {y}eKuUuuu9΀0BZ8|11'>@hAw(maûV #>w̠ #iBdo9sp q~W H Bqaw -@譯t&|ZPh%?͇{P`#Ov6O ̨Um# ڒȠ[B:+(rZszgBY쨘9 dzd%ŧ;hѥ`K~3TSGPk<ٰF)Y$GR:+^ HIdUˆ4QI%".EyEqaB@ZOTN g#qU B9ׯZ -kf^T:Y: E:*:m"BnP_Gh2\O3J\p^B ^2: Qh>mx(DѪ(Fe@T:-t7L /C600P#C;TP!'U -l|Q F4Hkl>>BY&uD}x~ ~Q,/yk7B_v%obB>:Ur ˋ>hlb$ٶ")O{LD=$6'O1[^śu0Ǔel.,Ϳ5Oޚɚɚw%N{[Gדľ扼P?4MEAJrOS}PCCCpPSuTu'5(wm8y(zT}W ~dߩ -800p("w+xBrs{^ -n6[ -VUhWϙ >V^c [ -jj?҄R/ńdPt%_-4ze~¶U< -O2|PN'[c_?.b(KRitCUvA]Om: y`6lmv`l[l[³ŸsZloqEs^ύnsmvGZ:ҿ:O| {} -Z>(l[juշ -`4B]Fp +X<,`+Ga`+cOVy^o?>>~}}RiYi+X -V#Agf0`C0B`*&]F! Bu=[kT;;wc;Fa~如4a ȻFVn {g/P<kvٛ. -oڤ -gK|lW!zOv_~Vѡu.f~dHWcnwlJMFGS"'=琴Op{Z /*[DJa/ɼ tdb zBƷFTzomgdΙ. :чw~dmc -[' &x<753 -y F`wf*~ԩQ0rTxow -AS ! ⦅$&KXU.ZM3>S li=;i򅒜ksv]0~ժ+gGAmEE83w&gf&df;_ꫜ m%x`>6n\ְ -$R = K, =m52~ѫ-"OѲM6evw&mt>ߡCSt~Q -e_p]3fK_ӻѾ=ڷ+zC(@*d,<[ `ݖ\1c} T0R6̃1He(dr/UH<9b=MVȧ= -pnUCPgڽ>.(B2a~> \3=׮hwӵA|H9\rOGmINGz001s괰`aWiqD#.͐o/W/#' RD<hr[ְufZJnf5z7KWwiަMy++#7o\vޡW [ -voXnABGg;9=pP%偞3 ՙB5 -FfGuxyG~+x՗WBfa3pui10't${%ӂ\:WnfqLw֯=Gp!=#LйK0_f635L</5ԙsvԾT^%=hϦ2:1:T< o>|e:z| hSi5a93`gRK?a//wwKM#eƺ2}l$bDD=2H"P$.T(@\%C -q,"ɿVcr*' *q(uho T)tn8MbBSV$ωI-Σyr#q]co]_bF ?ȸWvdqApbFADXccӑ~<{g3;&<#cujzz 4 Gsuu< 4߀cOߺzƹlqڃ|/4-]^݁EiMЦ&nܴq^ x@xV㝅U4 &z7AoquE4 ,gNKOO ا{3qrN|ո),+j.Z3Voˎ{?q[kUzu\\h_bĽ%ʎg;DoRp3 '=GCV \^ak0#8ƅqM}*#lG#^'Г^kfbѶMM -4Ӭ0ۏ;w5Mˏ͝0I&zb94=DPd3 -7pn̢<*,X"Y羑?WN)9@_`)P%RDZOxy=W[藇}]DCp:m Wz y ogb#] >:ͭ2pr2^ nfX(٘ -،ogLz*=o|oz֯Jtxkk⼽r~ BӁŪ0ͳ 7=;f˧$70*~Y.QSv|aR켋_ܾOtI2 3f# M636lfꘅ qL@Yzc"FE2@vz]؎;*[I11u„S96$ &Ι79iݦ租S;oԿ{9yK=R6}؝Y>%EӘZxk۶i~#U6i9 G#(z8B4UT^(bI9Ƒx^g>nI215D_!Sk~I| P(퉐"~1G〧ٓTB8?_g :"=G,\f9gгBiS#5t_3 /g9ZAȄkTZGڤ;+TI%%$ͣ>SNu0y۱gVYN\24ߥ炨!=U}}/|nVF?~h~x{@o<ɒa028O+~'eftZnGlEwL/m&z{kl3cFLeOJ7;JoNzb8{JjDq4~;81!<93;=1 wܺ½Cr661<|XJG_zUfˌܚ'5{|D E>>yU)7{ͻ3͟ș3##Lؔ[SJ|T)U_bCBg]V&y,efaOp? USA)}a*m˪Oia*&`MGXSfGo8dgwr7<|w>po.QѿnR_-KdRЙ͔b -64.|xKr~6MR/类*a!۫_Xf.%ɦR`(.5F(75}V)I $^$7@ǛZRxLh* 3Ma 65(1A6:Ty_(s^-m -O$\9bi~#?[+J@ݸ% -T6$1v9z.Ӝ0C"դTBϊ%g浔 4݆>ڷ&srq㦴J .LKpgK*q@ >~ _ -E7^"ź/ShQ![gE+quQ1sKsqxW؄oW FW?u -K -3Zt:QS8E-#A,8\u]::ixO: =>ɉ^ Xђ%17$1HpiEe 8.$Ka?v"ئ>N DF]E% r%u}|i%L 7ɥ6^21NӶOO H* ӏ֠FD@~F x_ @| /]+M-9߽|usWԒH cgM 4whS+x\]: [${[i%^aa̬i]I~t|tX{+ߡ#22^qrnYM;tD?GWԮ +^sS\v.fr>BI6M//6ǭ3ld;Уر$. -D[y"K^Dȹ`qnӪE-~h ]7rҭo&.&mtLȰR, -#凤 >"]0St''K4 m48x@2~\B,np-k!bˣ9~)w'w2gʣ9baA4);N퉇.iC!a~B[e7R}# -r kl_h/*:?v&Mj~BX~aQzd逳.VzDk6>h%:VmyUCUTͼ9kj4>h`KeJ7F E^J^gGhBQz@Q8\!R+Dh  -\`h3  +fA!7pL+M43>Ce "!PC"鼥+}W2 >gC`;ڹB(7=I-> FD@9+(N'C>52$?31-_`-/XWmҩWl -ޣ VA utE]Y'>xg0pΞm'bVV`nTȼ0F0*ZEX%BXSNGW/0_ƸqR_& Ve,o[BG vL߬-׻9oAYdaLɇ^:lxݪ~4a;MAz=g4@{~!A5?!B4=(f6RxW>3.A5B΄O +mPw; +BzSxR\ 73*$hE$2-WPs99 BeAY쨘9 Kdzd%ŧF/ x﨣 +G*]o,Owmt5pkd #i{(5lXj,#Vr1zIezX|Z$餍@*\AaT#<Тah r'*'ꄳՑ*WXx5`ga*W@9Ly  !74'~%8p`*`r]m/bt(K6҆:Ur ˋ>hlb$ٶ"©O{LD=$6'O1[^%u0Ǔel.,Ϳ5Oޚɚɚw%N{[Gדľ扼P?4MEAJrOS}PCCCpPSuTu'5(wm8y(zT}W ~dߩ +800p(w*xBDrs{^ +n[ +VUhWϙ >V^c [ +jj?҄R/ńPt%_-4ze~¶U< +O2|PN> [c_?.B[Q:JztCUvAOm: y`6;lmv`b[l[³ŸsZlmqEs^ύnsmvGZ:ҿO| {} +Z>(líN[~_U#`[ +V +Vñk+X +xlbb`e?>>~}e ִt ,`+X +AB30!!DsCq.!^޺N*ΎnCG;ZdhBxc{#m37U m/_ %re"ChY*v&n:MÑލ@ /L/{mh]a<2!R_HA8ei0aƹI(JUz9#t0JZ[߫e,{8ZB"vNI漒R,䬶9GKssH2wl_vc]kwNZZZYYzxee*\Esq́L*c#$i*'n:V.$߲-˦'#q"2@ѭ4.2{AcO%#;99am-=lK</1GR/n^-\=`IR{:z8J^><4'-SZ8ƍSO/:2&sϫ hRЫiDوMv|1OH^DB^e5,b瞧zJ?MƚO.dh5_!I Ӟ#LE$fԜl}6E4ي6 &99'y+BvpDV7awV͎mh25g/޼|&1{T% u4?Y_|îtaȻFVn {g/P<{މKulS.~voȃGU`Pq=:'lk+*j?Yy}o07%++%5+BWUU_^pn7ŋR9^p´U&9XdlȞiv^mnyoؐ!l~}S54n:N=EwPP6e +ׅ8j 1{;zLoѣ}{ᾢ1RJ6C9ZQFm3ʀJ*< +QBN^mRXT̓c-cDl%w.Ǥ[P%Tvo Pk1y1 '| >^ mP.oQ[s7wæ];?0,LI]P #Ksڜ\8iAOwLlBHi5uA#<2;F{^/ѣ_ +^РٸCwDw~0-nddZKm,ו.xq`I:~H4]̆u&bݔRf+cGF"FD#$*ER_%(cXw ma ze3QK]aҤywHt'W_ˤ?N򍈨;ϫ +.@%4"[By?!/"vmr_DdY[6ӈ +]|#5ut7 !uy30nR#L\\FQE +ыOLEԛtP*M4F8rHX+3-t8nf#U{\n ɉU*auu?{3rv1ëgAf,~¯7^z{#2mw n?;8ѭӛQ!HY.|0OٖȬ ͺMZJOA?&fm3Ѫ'~qYh15ȼ=htphQ;Q5 *RpP[!ج{&T>|?ŭ[;; +6SlV<|ZC4f|~H^V@+mwa\M__y<"}6HU9bD'؈vuggsd +P`cVS-Fc/!9\ǓGW"+Y\%bg"%h~N;ZEZa3Ij72vFeptHPl.VN%~jED#T("#܍5{$Fk(,BU(M1ke8 7|y :7s1ϩRĤ~y_IYU7G%WQÏm;42nih]+ٸ}`b*eQv:ةtޞpLvo\2 4ȑ\]--0差^q8_` "|KEw`Ѽpgg"iI6k?AjW胕ު{uvu8ш"x\{؃Ê3WgdSWBIWCݸvVy~'j~u5D-}qmO۸`*#e{lT.Zd1^ʾceG A~7DOs֙_Bt~pWz\XTTt!K.H01Kj¸bpug6Σ/sLSYI/53NL +tX`ˆ&peVpǝ;_<|fU +"JEs'LXdm'ML̈́": q97'jU% l7$PT#cv/ܣǐwС/< hC}~NtvJ!/ݜ="LlK#K*'', ;&nj!z(?̢2 +fWadO+@)#B,4hPJ +<[O0xSSmJQJ۱cӖO ظ45cJB bJylXZ}~>NpW_~ؚL3~7#-jGݙø^R4qX:97e<>b0Yap4"ɣXYOe-YLc'ٺ-D/QCT!;ǸEܞh\م_/Uh!{J"A_'!8CDs爅+A6^2!LzvY1 s\btۜKBF$,GK#qp}ޘFT\}g9=$\b8yPz +>2ɷ&o;̠*K׉KӘf_g}feOƓl!L##bgx^fCt N:OhiLOozpYtx~g|h_h~ig93Zf4z3'fڤ'QLGhʷC^9|3Oq;3QʇZ$-]ة'[Ÿ1l +zέRgXGNZßZL/~ȸ&Nм;c^{s5YX9sfd$}EXtj@T[IWO?ſKJ{Y^#:x0 DX= zegYRV6VA~IJZZ54*ƫ߶ƪh + i,at5ev$^FCpvI/w}wpg4K&e߲O&L٫[.plC/YH)gC^T.%rB + EaRb*,m|0D4tJI +/H$o$j^>:rǛdoDSiPnky_ J$%I|7ަZ^bDjiOS}"aMY+uFK:8(PCC,"`c9N!pOsΟ HVR =+WR2,dLv|wU֖┷Mi!J%6<80=!"V/Cզ3wc S&w),{Qx>Z8wnlƩz{GGǜ+)ȣɋd]aE^]&7lcԩ-)hG1PLz(|ԩwDDa>,$'z{`FKMƼ<ƈ#qq/h㌻4/`{0b/:-<Qv:oy31f.ȕԁ=SxH213@$zFJzVkr:cO>I?5 iԃt6{N?Z%7y3 Ҁ|F/{ur *ߕ&햜^>p׺UZajIT\ vӆwձ3r&n;`e<{u[ZE۴Y~0fVɴ.kL:DCA".žw般,h[sV`{Q QekŠrVT".Cf%CK䇶YϻPcMˋq $ +h5v,l2׮03r.tcqy[jQx 3v`_?Z:H׍t뛅I!2H!iebTE:Ix֒b3vMxnMe3OРx>u26۟*025ڋ ^686܅~Z(o%/nYhΣ_OؘN>e܉ݽ=%{;}c(D02x^hb:FzDAᴽrMѕ˰0ETΤCO( ,þ J>pJh-G}9z`p,cuj7 Zś{=E̛&~ȾFٯ;;`y T xo`Q.Z%UpV{(4šw 49"8 B +r 6sP h +B;hrLW؝ʹb4N>s3Z(b2? -BmP[Bw*s6 3+qxݓt@ai@(z:Ӭ_S/ӯH@jXN32 ֒r]/jxߪ=@kٰ?z \wOWޕuBs 'v"J'f%|nh FD `tR`Z^R/)j= ~{3e!5h"`u\61|5Ͽ/td0?nr4OidaB}vsF7= ܅­(Cs0GA\[l{l:]n͢hYo|Ѯc~v mv ݀ICyw|o[ެl0Ԭg}}?՗_Q"@qEJ endstream endobj -1056 0 obj +1064 0 obj << /Length1 6672 /Filter /FlateDecode -/Length 4360 +/Length 4359 >> stream -xYy\SǷ{oVj[ b%*\jZ - - wPĺ VEąuZkH& y>ow̙3g3aP844h>Bw|4>qCQ1[0b rYߏDHZ3_j[*qz<£ȭ;ru3\Z÷QԆ>0evpT{"B/Wb-Qh& Q믮2WtM7S}lɦc$O"Ț`TG{Ӓmy"y*R_ / 9>{"x4@ա0"钸qPQ¤9m%B 4\e}g¿Lx*J˾{h ^㣑J)(!PAe!DB U; z&*PRQGkD(P=? cQQ8$4ǵ~RO5Π)h4xCHǻ)d;zLϢ ޒ;rEM% m[J"9es?"57s 1(OLռU;v`#}|zFѻ` h"f%{ Bb7 CX!YO$ٽ 4ţ<Ԉ?I6(KW%Η{[m|Os ) -J8#<Oh?%=Ҟ!ORyiMyu"$A̷8pJ{I6Eh-~}#Q.xb(qU=krР^^ýfI#-M@KJI"u$^zp٢0Ў(] ';H}%Y˟{t6l+J*؆4ZCÃZzEb6=`_ -F_(GR&h5d6Ge]Lk | --`u'Q" -)?J^j]yboHbJ2NZա aS- =H:UjagL[Q6T5/_fᏉٛvWsG|mʰC+ii :@yr"3v')B\IIAVp`L`6qQ%Tp6 J z3ǯ0~y0/W+91c>[Y^tS_N|wRn?.>=o8@KMma%QF흜 PG2/RW2J~u>Ď3P?UjWAOmޘqhC [ w ˔-6NAkc08C1y[bzB$6ZD|=!w&7  -ɄٔdPZ5}->} }kj~S:㋸]gW2%~{]Om '?]2"`T?U :p0!6pRsSi7&xKGwLQK<|F74=ww{h|^{WqW -c>֪UZN\Z]n w=<3%bL}AUj4pU2-9ʝDUJD 91su{\.^=* S )A%)џY ΐKAR0j㹛⅘؛ngҼ؛%ȼym{s5- a_5/ƶ'Ջ8[HL<KúKb2JMX3cTѭ%w+k:\/r_079TKəuݭc$mv {&ӌhV셑6?S%XδG`.99)^_. V"טּ0`1hdq٣,\c&}toLUE'A.I^DS%R8VVV5|H7!3fcB)T6aΒttw8T@UȨ C)i'i~ι!Rn];2u@^4 Krۯ=8*we"ԶVQ\Rj@ 槲* M!Q{vD8\Zpɓrzͪs_xc78#+=zy`ׁ{{#~Z"#AMqZïO@Zؘ0`D` -X|˘o^yYTe":CLYM*6=m. F@Ep. =@0.*(Rofc4"\ `-![gw9>̈TDM]J/q9J:`5urJO GҵW#-gnxiexgj?i?pNHN\a+"'$FK^?~gQY"Mv-2쳐[:W/E?Q}•#ILʏ)z:r_h$Vu@^k6[NV+Nl;h%s&f@>FBsF0XѰ[V 497/B0HRQP_^._x7`6d\ow̙3g3aP844p>Bw|4>qCQA1[0b rYI# +!Bf4Р(~aJmx=rcAug. } jCAu2;(*A=c+1q(4`(Wt+ kX:&f) l:F! I#ixzZ-O$OEJ]rrb?6GcO$9:J!]׵8n\  +ߓ P4>_( L7 OEwk|4C)%0(;,>(! +pgA҄PJ*h(g@qUH@(hUsQ9MO#_q\+'Do=_ F3M|?9"yW4lG1PrY[b}T6!(7)IAH}܏H%Ap HoLd(J;9d5ofՎxwd_`.{h<>YIł }h4}> gilo?Iv/d%z@(5"#Cp>/f"<Ēti|cɁp { B.o +`!RX%Hd䟒tuiO'Li٦< :j [8V$" +qX(<18;J\Uk{{4hpYH xRkңy/\T8lQ_hGk|ВHe6]k5764- + +a2`^12>MOՁYks@`7c@QOR4X!ra?7D +̳I-BW ʤwٯ™;(koeTqj i0gjn>\{,N#374GRM[Х mm!P*3NJ/:Vk9 )nLON/;sDUJ581DnBe8=G[?>9lx7oYzz.СA&aW+B໛#a P_785gFO嘴~:{8s*㐳-ٴ޼6%2NqzMT,+.^k~W6^=|c@ٜ -۶r#}$by=N!,*<2';d 鴝,pCD' >1-Ex؅-}Ug6-Y dv;&0Ȍq/{nW@RRX,_l\@s:d$5MÄxLi +_%j=0˕DJdN̘tg29ٛnˉNqDžDZ Gh{^#HE +U_ϣ҇6vJǗ + +<34a2Pxkᮙ?zŦI19h{ 77vxftg(;_5O6pKLOHaV"d2c"Q10dL'k1c[[T$Bv{_l[2|XvTx x ([b ytܐ3 ۳Ąk~:/"SB/Th +hӍh@ 0ƟޤVCws4ssw.0_Fo1[%dL4c29%U(iWL?D]bỲwsRч}0!SZlU+11 ֞wu-dz~+~OcͶBpKz>ΦfZ.[+b)5gpt ":Xcm?csUidyPra;):Z#b+-4{XuL3=IGͅBIٌl钌L@ 54|Unj7p.e:0g󨹶4 l=v5JPV,6>2eLĮ 73+u~vUjN)p_׋ѱP@{Ӆ@!0FQu0 +wN 醫MK׏7[]P8cZS^:{KݍI~P{f^mǭ^ݾR*XV@o ZtAzXg|K$8`+޻i4eN[.s;h/s"*;,cy[m{8\VT3{UN RJR?ճ}!n9`2VW+c's77 17oϊ+?y}7+;Ky>H۹DїkZ@þj`_`)mMOfZq"ԙx+ #/=(cgSRXV6aě-SFߗ\ܝR \Gz>'ɁgZO4M'c#o}#ݨ%i{O}֘Mc7fDb/'!Нt/r=sy>Ia:HZFrRSwL,K8eZkC=r]λ^,r9urk #䔤2pk+G3[ +LEA55:c@cϞȓ~ ++$-WD`!7]OH̽~'y< +Ed57W^|m[Dgt^߷+e'c%1+G7^STuFQ0ZI[%2s3gpګ`.  [+~Ghd0l6WAj:.5ėbz2=J[lmik(2YsUܔ!"ހuKbf? nn5+|f%Iߚԏvc$)q,>/O>1=)JaiU=7cu^N@:\|r_QpF1( Ţp4 x! .(z{$ +h4@P{DVCV> stream -xX{XUU_{}˽0)R`h>R3sLS1T$E4Y^jTIY$}gN}5}9Zk[{ ^ 9IiN?QWg:'2,#X n/ * "V>vV [.5aȸC3==^ FD {q> 7`~>"%xxbsE\\Ϣ,Ԭ/=sHn ɯuoJXMƈUwВp6w§*u)xRT8uu?źĒIvي${*O!Ծ2z0 0_ -QE rHl"`!훽EXfayv,ky?$y tWvX.众`ܩe< +xX{XT_{}f@MaLFi)3fZY3|c x")%X⃊j\-$fpܵ`x{9Zk[{ ^ 61itl;zq. c=tFdX4aOð n *s"~2}TW6͝6ç VK\lucX972.~TOHQ3®xmGG߽qY8b\0G􃐅'ǥt"^wx_HR6Jx@%׿K:lV yφ"< +syV.K=0 i{(I x2Q]f>was  =iU9g/7YU e4h|- `X!C?xpEhgLflegKxêh*oabͶVA6?FScS79t~}헔nd'{>b\ Hт+h!h|zڭ/XLVdӝ W殒_]&ky潐XI)Ў2:5fYXI+*Jp(Wr/r)"c#Ҋނͮg߃4Mr7gjeV>A땽}O9^c͑kݺE\2?;x \~71x˺=#V"nD.g4k +`{@/W5C Seo&ɸS!z$1=S޳5֮PeYG +e%:f°}nv<Y/31M=:AL~A΍/=8Nzb̨0.I)yk>o2\i$KJz-DQpq>06= x-'j4(J n^{s~2t H7V{kێnĐĶ+w~ǿ'xŎ!olVɟQaL+Puv-I|=]Krq89WNe}S18ቄ,FzV kdwE=ْ2oO"A += dU U#Ǣ >;WD${EQ% +t3'Fߘ:Eな|ry)؁/P:d?FV'fxB9(9IxԔ)ቊ~0:f8s.r$EP'N"Yw)7Z2gt/$&_?*NRC:I~!mP@{`AM# }ۣ!ң*5"g4k] F %qN L-JϒhYLfAJح\|k""@&žV4cc֑ yqWc\Dfjt,;v=W+ъ؄?-o\ςMH_Bv')W:0=ۆJ%zF#²VLM"<-]w_)yñBǚSy%#A՜?3x(R 3NU_iݿVm{}Q9s4nRv,(13(sXaB=-aPA xѵ^,Y6}VF_ByZu]{ʺ&Cn\ݗ˿n" ;ux|wUJ^Mi -n$✆'IX2QXjQgK̾_&ƴbW1 WSudiu_NSK3Qi+_*xߢJ9AZ4@OjB$ z2'm|ew1Mnwl_?cV\Gg픚})3@C@Lz#f3jCJG9ܣN+g'MJ-ߓGWS]K!Fm.f!ogZॲM*NzkBnqxCU3;:;ź$_ު=R t^ϕq'<1J.w<KU]M|7ֶ(D{ݞ=%EÆvx>cv]'=jq]{`g("}|v^mZua2~*څ*DhSQߟ[zePsʎY~t7sFmޚ:%푪|r}9؁OP:d?FV'fTB9*9IxԌ)ቊ4:f8s!r$EP'N#YoȐ7Z2gt/$&_?*NRC:w,6dg=I1>m$v;TFFr h$C3IE -V!\>XI` CVdWHķ7Ъ[flw:$/lb\sQₔ%uG8YPc3|;ZP ;YZ߯t2HL;JGfgېBWxPXd@hDX6 _&.%;6VP@y*ĿPo^Q1*0C[KKξ37`=vR~.Q#;x ˎ\آ4eCxt=6~eQnF%'9w=u+y"k, hv+?h&Aʮ7Ýt;`A]p@c8G -h!]vjɪbI_8WYb“HϱO/6z!SVLYOwW"@t Tj0X#+* 3j0~7] >S!;6SKh*(PR7"3tt)nIMpf[]=- omB>7Qi4۲lͮw(*D<~j$(} XتV-,%f%Ó><77AO}blZ-kgeܥo Ѱb B?h<FcoG<0o>x[샷նYjm!e#'XⰈxn LQ(*`+ޒ B4עK5B/!vZj{o}3m y3`&*7Q +h:T;ͷęǜ',eyg1jhkWOK.k4k}-iM=a{Z%ͦ G:4@-0}_/*#U x)vϩMMSk~KYow)['۪V#0mmVZYHk>gȱ8{:8,w|#q8? +5-؊䁷8m~5cA!BK 3wo|Lpm>`>d|^LJAE, Q endstream endobj -1058 0 obj +1066 0 obj << /Length1 8452 /Filter /FlateDecode @@ -15145,45 +15353,51 @@ endobj stream xY @TU>k3 %OPIF@TI !Q@P$E" .""" p2|4>ȐLȟ2QKC|0kAas9kZ{3p\zsY|s|@~E.qW8g${9v$Mǧ|8 -ǽ,R9";ccL>Ya{Ll3aFё)߃"'qImU ^qmڤY+a966^瓢wM{sZ=v9 -6u/%ш5 x-pD!)?tW*C,to," /?X00fH%@Wz,NNh! ZsRIԍ^]HygHH:~&t P($|z$~n@J_E -yS}g.H:8,˱fN\̈< 9:4= DTRǣ]a%^e:Ц OeHb[&ό9.;/#sf*g1~8"4))逞i9%99H^XP*8'/![~oϯ?kf`*JMS@#)|.iܒ -R+W^[bD-02nߌ\(637vȂ!k|>uÆ:~hкLM^7ydV{L'~w Ĉ"͒6=(y9ozuSMeZWͳr3B6̈.K(s -K$#d"WWT衿-~J;c+V(RK,?\+QHFњR{B@|Hk -!R+Nمaswwd.Z[ .܆1wttᦰ5>4>JjDz/ؔlzF oV.~k_7?t&4_|{y ƒ}gad#gh=cϩܑϜ]|Y7C-_2qGӾ?MλFwr?5y?v^a3FxQs+,Q甁V|ԂcRbmea#w$n,<윊%%;Щ7Snu?w…L2 %VHn6@(URtZ :- -㷊 -h5sHCb0wix'2OL@|]TͅԦMJsPW0ZH962 u%]MPLUFbz<"z5eL q(O ٢P%(#i&rh`"mF$1jg$mOI?ߋn"w'f -W s3$>9M~9[{޽G`RNH`rxY6W/ƫŒ 39(EBA`#k.Zߧm*~B~9b}[91s[*rHr -F)}3C/%-yL*'Tz0}7ӷΚD/#ή}&jCKxvPS@X`}7;㍋靇˿_UTUMͮKձ_kIotq7w4MQAiՆW^9igwh~S"ڀ -X"}tұpUvmdYOAyYGI,{1Ljֿ Yh D#a2txq.,YeKag,gHgYro0ջ0h30DfTО&\lk~mĸ7ǾF#82 r[[x'0QR 3:f!@Hu -2=KCeT⨃!BH+u^,k_'1=Hf;H0"?wW|Z=-R<+LwN -?{cvx&(*[tjĥx|vvXЬ^.*QP&I~?,nl K;'z 5,3Ⲳ2ls^u_1zi{k3ҦmXZYYMo%~pj̋'q:wF1bv*!m:~ZViƷHZ?rh0Pn5Jf٠`;V߻avV >.'ԇ4h; ->#1tzRjeWGZ ohj="HϬ/)t+0q~e(A` -p>fXE| 3`'y>p𸮟u>ppOA -cP`t喕斖>j{iy!\KoHG7L,r6A.dLy_ǖP`k.Px{˞G}%.撜⪮V3М^*T I -^2~˿̀zLL/"0Ji\B7FĖzFD,}FAZtf~Z&>핎;sVU4R͓m)TyGd.Xɉ-c0=a' ,}Y]\RUJQBgZRyy괜ѱ=Wy%k7E]qN/ -B%̰b\d::c@+7NGi0qJ.5 AE8R[;T}qY)''??'gBf?[h Lbc`ښYz` &\pjY4/V Gj\€C"J:Wy߼ ظ)ܬܭ2rkͿjQTJp.ijkCC5/:6a\a0F` SB G#l]@tQaԉkk'V'zc;T+FP:Qqo:PCAUHЬzlkkKa!?f$ PVOHBJB|v:kaߖt ]F窉jXFF婥V4h!Z2+`r(7Tj+EKcGsjG5_y6V7mv1lgF_+|s|V{Y esN:{rگ u?]i3| lX?!DT ׉7<enٶmKmݏzow߷]}{9 - -P Qp幄 j)d[m-\~EocniX! [נ=ܜ\e ݷ1{8A6 zEKpdQ^WM=VS7;x Q~םO_k[2=(7ՇtpB oV,JO ;WpEV.?xƇs>1J<2a‚E~_*O:%ܢfN$/goll,٫PsSs4<P~AДlM{|eŏצl)w Q՚wx5|1IyO\un@ܖ Vx2g[CڢVqo5;w.x֑?. XD-{Ao}7u:B.. -I\T=Iu\̙qO o5rw)\^Gt'p͟^-Y;ջNb}c=f0)SRr|0y5^G|ʣ~~]򨿿Ѹ.Ŀ;O~#wճH4GUH8|OFDQ+Pp Bm-NEN^z~~+e%0^gٛs?~ +ǽ,R9"'1}t&=&6a a0ȔU 듸$MƶݪȄe/cvlmRb,ٕ { 5_뵀{sv;BhšxxI3b~qzN'unfDRR]kN"EcQ뮰mwJ}HMHhS?c2P-pgc }בd]q933 ݈?~At@Oq4pΜ$` ?|(z“QėJ-?W|3 X0f %aJ)Y J>4HnIU+]/-m1"FӍem7oFx[xb;d5C>:aCz]?th&&gI=aLLMMWpDbDfIZ漜7WXˉdϺIҦ]Mw2m-EFXȫ YsxZ!cqfĉ %jnxWXD[ʑdn2@+؇+*V?+zW|K)]L{%.i$|D#hM)J! >^5GM['°Mrt2JcNnÇۘ;:MrpS`nm%5"GVSlJwJiR6E_jl=7+/OoUIAʾ0^2a4۱ԋwHg.]`{!]B>*灸rrRNmNRܞcD\ +Wܞ6+\==*yj Xtr|.~z9ISB/~5l(m#iߟ&x _VƎC#l;9ΟEj1F);I|JNvvNΒR_ԛ)7;]&NzIp k+b$wpD L@y]:v| |[E|g9I$FH!1W;δ bř_&KT*U=La[gzkg׾~%n/*fWYY~yƥ/絃$u;c&(jCBr+~4m?i )m@D_V,B:X*}b6W2ZsQ<ìQxǣ$@̽&ڊ{6m&= K3h]}@OdA_Ĕ?nߥ.N<\YQ23FU ANX88p uٯ^Syr-Y%%Y7m9D;o8gS6@6'?+*| W82T"joc-i=7+s A?c39ƬWZe_~LݐIwn\sKޮŲ#5--5GdK5PQ,V0k:AD8,䥰`w$,7] ޙWBvLC F*`hϿaLi.MŵQzI6bܛc_h @t--< (t)H[Ukb n j :uǡ +*q!E/D +d$ J_ȟ;Fëhqb>{ lKS)k ^Cd'D=1;?;,hb/U(zN($MbyO?]r76YQ‹qqYYqq[9d/ں/pE|=iSn6i-O,kv醴gx?8}׌r58`e;r| [`#|C{FA1; eLLX͐6\? -[$rz 94UU(q%cyÍlPpd{0;_ggB{Ci:=)}ꫣy{7W45ъ?l$fgїC82ޠu0FVU_P|`ر`5ze:DZP?Q&7!3# \*RsoąL\qu&lV_x5^"xj^AeTB'w_=rŌEBB4(BuwMJcAo5 C/Ťm$[ #XKWS@2(,QYˮ6RҦ_}*7eAX$`)[JDSgw5*cAs/47,VHW7̃M ZzFؘ8'Cuү^NNl9 ;^X}gc͊/ǖR⯜:ԒS嬎ѶǏ:;/-].Kv~V*qd "$Mi=bD祯wpȍ5Y@::7K Z^6̴gj +9SKcȩӦMjb=7c)Sף~ Raq`Y$i~_J⌾ܦf%FҒfZwijRϛ(:4yLԆ]D5Ly~HSڡc2QhgiI6g?JW!Q RiFr%ۘo *Hk+i+KT EX6l= +X7C4XjUDS [5-= +AD&C7V* +L\ZYX?xr."a \1B>M!E+/4A -}.4!0P_˅4XVj7]sCCp/2?G8lh̩z/G;X,XmVsΏޘ>iBbo  +e!stb&wlٸyƜMڮqcou7)K'ۯ}c;Y0{_PoWn@= ^+~Fz&_w)?_i1u:NC؅-Uti8 * ʦۍM99992݂EK`*ౄ|wm9`m-귖Zƀ1͇:s(lŠPGjM9Hxɞq` 0T_yphi'[x6SK|Ȣ |!*]C_ ԵK?fb5?WMUC 22*O-ҤA YYt@R[-Z;dХW?>JA'xOqnC`[=3_ᛛX8w'Ȣg/;vjבߓ~fI*DDLc ?f !MN/3$v˶m[ +m+~{l_hV:4HX:8uO,%u'N8VcL)%mth%+zsKx7 1](O=b_~bQzZq߼.:r6>4ч!U ,Ryz)\4s"yѶ8c˘f#fc^򜛚)7U,HڊHD!L7dQB8ӡLZݐdj>|VC5s&J$:RyiP"!B܍\yw jcu{*ѧ9, 12G3aĕŰN KmPK$DhoikA4|,e[FZb`j0CNx#~氒ao7ިؔ3񔀀 Bg#hb+.~6dkNa]C; w{0%Nʳ~"[tf|'L^>[ +]{;qp{qaĂ&jg߻JG?|} rywAPHРO +f,~2]\|먑[<..,cMڝ:Jt=c=kDn`7NRo +IuA0GLI]':| 5V?芕Geގv%Q3kȗbm_:Y}~~+c?]N)HА~ij76vtm\t/Fp7RZ%rI\*[-b ++ʽÎrB{2J%:]xN m>ohq/rlfc0[)/: \} endstream endobj -1059 0 obj +1067 0 obj << /Length1 1184 /Filter /FlateDecode -/Length 698 +/Length 699 >> stream -xSKHTQ}̘t AK9p lQ -;gpPԢU-zUEiS Ii;}8:s΅P{He@IZse(%J86{pC_LO^ ! -PšX0=TN˓ ?Pi(z$`>KuѸOG#5HQI;6uo;O'SK8ݦI'g=O+Nu~G]  ER88y^"Y6RR'OrY -namf*ؖ@Vm$)?{oFa8mX`zQZJ RC)t +G̚^vVl52R{7YRmp:T-;ɸ)^cq1W>enzfjKS3wVctavl&S"]V AWQ%bmD"0B#SiM[MK.rRHRFvzvQN \I sBtS; %G,!Q7k25Ay#in ZU"|+]dE_1<-Ikܪh%r=Ε˼*y0oD⻒{c/sȽ5UB?2 +xSKHTQ}̘t AK9d8 +FejDc՝Č38S(BDjѪ=!*D$M"HHM; +Zԝ{g^{ιp2zL>( Zwtvėb Ԋ^O!=0  p@o3LC#y ĵXj(;'n}:Aå:%Pr[=yݺ~DYVH? Y^Ovgru٬n٩H S+)׽@t+3 ƈ[$.CKh se'7@QFxoMxՕU +k&*q$0ADB)J|ބnj^24B2tİ ;rjJZ(=\d棃If>ruf YL_c r!Oc|uu˷6OU%·NVܿcْƭjMύ_"W\̫H1?F$+9~bW>4[3YEaij1 endstream endobj -1060 0 obj +1068 0 obj << /Length 1178 >> @@ -15270,7 +15484,7 @@ end end endstream endobj -1061 0 obj +1069 0 obj << /Type /FontDescriptor /FontName /HOYATR+Roboto-Bold @@ -15283,10 +15497,10 @@ endobj /CapHeight 749 /StemV 80 /StemH 80 -/FontFile2 1054 0 R +/FontFile2 1062 0 R >> endobj -1062 0 obj +1070 0 obj << /Type /Font /Subtype /CIDFontType2 @@ -15298,20 +15512,20 @@ endobj >> /CIDToGIDMap /Identity /W [ 4 [ 249 ] 11 [ 162 351 352 ] 16 [ 244 ] 19 [ 373 ] 21 [ 574 574 574 574 574 574 ] 35 [ 498 ] 37 [ 673 ] 39 [ 654 650 562 548 681 ] 45 [ 291 ] 47 [ 635 541 876 706 690 645 690 638 615 619 658 654 ] 60 [ 635 618 ] 69 [ 536 563 521 563 541 358 571 559 265 ] 79 [ 534 265 866 560 565 563 ] 86 [ 365 514 338 560 505 ] 92 [ 509 ] 203 [ 562 562 ] 214 [ 690 ] 229 [ 540 540 ] 240 [ 565 ] ] -/FontDescriptor 1061 0 R +/FontDescriptor 1069 0 R >> endobj -1063 0 obj +1071 0 obj << /Type /Font /Subtype /Type0 /BaseFont /HOYATR+Roboto-Bold -/ToUnicode 1060 0 R +/ToUnicode 1068 0 R /Encoding /Identity-H -/DescendantFonts [ 1062 0 R ] +/DescendantFonts [ 1070 0 R ] >> endobj -1064 0 obj +1072 0 obj << /Length 1863 >> @@ -15378,6 +15592,7 @@ endcodespacerange <0054> <0071> <001b> <0038> <001c> <0039> +<0028> <0045> <004d> <006a> <000a> <0027> <002d> <004a> @@ -15389,7 +15604,6 @@ endcodespacerange <002a> <0047> <003a> <0057> <005a> <0077> -<0028> <0045> <0036> <0053> <003d> <005a> <0031> <004e> @@ -15403,9 +15617,9 @@ endcodespacerange <002b> <0048> <000c> <0029> <00ac> <00ea> +<003c> <0059> <003e> <005b> <0040> <005d> -<003c> <0059> <0025> <0042> <0038> <0055> <0004> <0021> @@ -15446,7 +15660,7 @@ end end endstream endobj -1065 0 obj +1073 0 obj << /Type /FontDescriptor /FontName /UKLFXJ+DejaVu-Serif-Oblique @@ -15459,10 +15673,10 @@ endobj /CapHeight 728 /StemV 80 /StemH 80 -/FontFile2 1055 0 R +/FontFile2 1063 0 R >> endobj -1066 0 obj +1074 0 obj << /Type /Font /Subtype /CIDFontType2 @@ -15474,20 +15688,20 @@ endobj >> /CIDToGIDMap /Identity /W [ 3 [ 318 402 460 838 ] 8 [ 950 ] 10 [ 275 390 390 500 838 318 338 318 337 636 636 636 636 636 636 636 636 636 636 337 337 ] 32 [ 838 838 536 ] 36 [ 722 735 765 802 730 694 799 872 395 401 747 664 1024 875 820 673 820 753 685 667 843 722 1028 712 660 695 390 ] 64 [ 390 838 500 ] 68 [ 596 640 560 640 592 370 640 644 320 310 606 320 948 644 602 640 640 478 513 402 644 565 856 564 565 527 636 ] 96 [ 636 ] 107 [ 1000 ] 130 [ 722 ] 139 [ 730 730 ] 157 [ 843 ] 162 [ 596 ] 164 [ 596 ] 169 [ 560 592 592 592 ] 176 [ 320 ] 182 [ 602 ] 187 [ 644 ] 189 [ 644 ] 277 [ 989 ] 1332 [ 387 ] 1928 [ 318 ] 1937 [ 590 ] 3314 [ 710 667 667 ] ] -/FontDescriptor 1065 0 R +/FontDescriptor 1073 0 R >> endobj -1067 0 obj +1075 0 obj << /Type /Font /Subtype /Type0 /BaseFont /UKLFXJ+DejaVu-Serif-Oblique -/ToUnicode 1064 0 R +/ToUnicode 1072 0 R /Encoding /Identity-H -/DescendantFonts [ 1066 0 R ] +/DescendantFonts [ 1074 0 R ] >> endobj -1068 0 obj +1076 0 obj << /Length 1150 >> @@ -15550,8 +15764,9 @@ endcodespacerange <001c> <0038> <001d> <0039> <0051> <006d> -<005c> <0078> +<0029> <0045> <004e> <006a> +<005c> <0078> <000b> <0027> <002e> <004a> <0031> <004d> @@ -15561,7 +15776,6 @@ endcodespacerange <003b> <0057> <005b> <0077> <002a> <0046> -<0029> <0045> <0037> <0053> <003e> <005a> <0032> <004e> @@ -15572,7 +15786,7 @@ end end endstream endobj -1069 0 obj +1077 0 obj << /Type /FontDescriptor /FontName /PYKAXF+Roboto-Light @@ -15585,10 +15799,10 @@ endobj /CapHeight 710 /StemV 80 /StemH 80 -/FontFile2 1056 0 R +/FontFile2 1064 0 R >> endobj -1070 0 obj +1078 0 obj << /Type /Font /Subtype /CIDFontType2 @@ -15600,20 +15814,20 @@ endobj >> /CIDToGIDMap /Identity /W [ 4 [ 243 ] 11 [ 170 ] 18 [ 239 397 554 554 554 554 554 554 554 554 554 554 ] 37 [ 625 ] 39 [ 649 655 569 562 684 ] 45 [ 266 550 ] 48 [ 527 865 710 677 616 ] 54 [ 635 592 597 ] 58 [ 617 897 ] 62 [ 598 ] 69 [ 536 554 515 556 517 331 555 549 224 228 490 224 886 549 560 554 558 336 506 321 549 481 754 486 475 ] 230 [ 517 ] ] -/FontDescriptor 1069 0 R +/FontDescriptor 1077 0 R >> endobj -1071 0 obj +1079 0 obj << /Type /Font /Subtype /Type0 /BaseFont /PYKAXF+Roboto-Light -/ToUnicode 1068 0 R +/ToUnicode 1076 0 R /Encoding /Identity-H -/DescendantFonts [ 1070 0 R ] +/DescendantFonts [ 1078 0 R ] >> endobj -1072 0 obj +1080 0 obj << /Length 912 >> @@ -15681,7 +15895,7 @@ end end endstream endobj -1073 0 obj +1081 0 obj << /Type /FontDescriptor /FontName /YAMLPF+Roboto @@ -15694,10 +15908,10 @@ endobj /CapHeight 711 /StemV 80 /StemH 80 -/FontFile2 1057 0 R +/FontFile2 1065 0 R >> endobj -1074 0 obj +1082 0 obj << /Type /Font /Subtype /CIDFontType2 @@ -15709,20 +15923,20 @@ endobj >> /CIDToGIDMap /Identity /W [ 4 [ 248 ] 11 [ 174 ] 18 [ 263 ] 21 [ 561 561 561 561 561 561 561 ] 37 [ 652 ] 39 [ 651 656 ] 45 [ 272 ] 51 [ 687 631 ] 54 [ 616 593 597 ] 69 [ 544 561 523 564 530 347 561 551 243 239 507 243 876 552 570 561 568 338 516 327 551 ] 224 [ 544 ] 230 [ 530 ] ] -/FontDescriptor 1073 0 R +/FontDescriptor 1081 0 R >> endobj -1075 0 obj +1083 0 obj << /Type /Font /Subtype /Type0 /BaseFont /YAMLPF+Roboto -/ToUnicode 1072 0 R +/ToUnicode 1080 0 R /Encoding /Identity-H -/DescendantFonts [ 1074 0 R ] +/DescendantFonts [ 1082 0 R ] >> endobj -1076 0 obj +1084 0 obj << /Length 1626 >> @@ -15841,7 +16055,7 @@ end end endstream endobj -1077 0 obj +1085 0 obj << /Type /FontDescriptor /FontName /GJMVRG+DejaVu-Sans-Mono @@ -15854,10 +16068,10 @@ endobj /CapHeight 759 /StemV 80 /StemH 80 -/FontFile2 1058 0 R +/FontFile2 1066 0 R >> endobj -1078 0 obj +1086 0 obj << /Type /Font /Subtype /CIDFontType2 @@ -15869,20 +16083,20 @@ endobj >> /CIDToGIDMap /Identity /W [ 3 [ 602 602 602 602 602 ] 9 [ 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 ] 68 [ 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 602 ] 107 [ 602 ] ] -/FontDescriptor 1077 0 R +/FontDescriptor 1085 0 R >> endobj -1079 0 obj +1087 0 obj << /Type /Font /Subtype /Type0 /BaseFont /GJMVRG+DejaVu-Sans-Mono -/ToUnicode 1076 0 R +/ToUnicode 1084 0 R /Encoding /Identity-H -/DescendantFonts [ 1078 0 R ] +/DescendantFonts [ 1086 0 R ] >> endobj -1080 0 obj +1088 0 obj << /Length 337 >> @@ -15909,7 +16123,7 @@ end end endstream endobj -1081 0 obj +1089 0 obj << /Type /FontDescriptor /FontName /WQJRKZ+DejaVu-Serif-Bold @@ -15922,10 +16136,10 @@ endobj /CapHeight 742 /StemV 80 /StemH 80 -/FontFile2 1059 0 R +/FontFile2 1067 0 R >> endobj -1082 0 obj +1090 0 obj << /Type /Font /Subtype /CIDFontType2 @@ -15937,30 +16151,30 @@ endobj >> /CIDToGIDMap /Identity /W [ 13 [ 523 ] ] -/FontDescriptor 1081 0 R +/FontDescriptor 1089 0 R >> endobj -1083 0 obj +1091 0 obj << /Type /Font /Subtype /Type0 /BaseFont /WQJRKZ+DejaVu-Serif-Bold -/ToUnicode 1080 0 R +/ToUnicode 1088 0 R /Encoding /Identity-H -/DescendantFonts [ 1082 0 R ] +/DescendantFonts [ 1090 0 R ] >> endobj -1084 0 obj +1092 0 obj << -/HOYATR 1063 0 R -/UKLFXJ 1067 0 R -/PYKAXF 1071 0 R -/YAMLPF 1075 0 R -/GJMVRG 1079 0 R -/WQJRKZ 1083 0 R +/HOYATR 1071 0 R +/UKLFXJ 1075 0 R +/PYKAXF 1079 0 R +/YAMLPF 1083 0 R +/GJMVRG 1087 0 R +/WQJRKZ 1091 0 R >> endobj -1085 0 obj +1093 0 obj << /Type /XObject /Subtype /Image @@ -15975,7 +16189,7 @@ endobj /Columns 4969 /Colors 3 >> -/SMask 1086 0 R +/SMask 1094 0 R /Length 3704458 >> stream @@ -29272,7 +29486,7 @@ p5^vSٲ 7Y[lX} 3"% 3"=,LXKxR68av)j>-+ܼfloago{7xl,xp0ub.yVIMYpEˆW0)]0)يuY'eӋ+6mWfbǏ[ko]?}tC7`ah\*=,*sczɉKصE8f/=^0~=B \|fp&|]uɉ?CBgg? V/;9Q'c[ge''d b /,XD_l331_-X|fpF`2 ^vrO/bςNN_,>38Yzɉ?CBgg? v??ϟp?LzO.N??yP-}YRƧ).S~E:cn}E7}Q sf/;ȸZxCs鎳}cn Ң_|W+jэl_veoy?_]?Ӻm]?q8.)~q|‹i߸UNF[9s1uGooc6 u|,yqc];fg^xQ"WHa3]#g2朹 wӆᯎۘ%/xLkl /6S~3W9);l=~cbAƜ31au0uwMyEf4o\y*'#etuZ9Șs:#̷7ܱN:nc13/(L+\dQ3s\x;iWm̒W<&ߵc6}e)Xq噫?1j1r cΙ0p:mYv̦ϼl3E7‹i߸UNF[9s1uGooc6 u|,yqc];fg^xQ"WHa3]#g2朹 wӆᯎۘ%/xLkl /6S~3W9);l=~cbAƜ31au0uwMyEf4o\y*'#etuZ9Șs:#̷7ܱN:nc13/(L+\dQ3s\x;iWm̒W<&ߵc6}e)Xq噫?1j1r cΙ0p:mYv̦ϼl3E7‹i߸UNF[9s1uGooc6 u|,yqc];fg^xQ"WHa3]#g2朹 wӆᯎۘ%/xLkl /6S~3WYG_5( endstream endobj -1086 0 obj +1094 0 obj << /Filter /FlateDecode /Type /XObject @@ -29529,7 +29743,7 @@ mp5 QO\K]޾U/////Oo%_n;>Wz)p^k<^CyNOʾ>y~2}8gxw';////D&/1Ƕ* endstream endobj -1087 0 obj +1095 0 obj << /Type /XObject /Subtype /Image @@ -29544,7 +29758,7 @@ endobj /Columns 1920 /Colors 3 >> -/SMask 1088 0 R +/SMask 1096 0 R /Length 782043 >> stream @@ -32278,7 +32492,7 @@ wض Z\y|>x(,&cPYYPs];"WB!B!rS뺦ibU#EwWff"&էz媔eIc%%{iQ`~'uY}Ov+^Vm[$Y8ѨGs² ,I8"@8Ji6p. aBP'܌Ȝݖe !FH$ڀY$qu0(B!B!q!c I˥1E4۶XmD"QgĄ_rQVVjd@@"7o=޽{ܹeEd!lӴl[`RY2 zr,˪Ku"A 0I";nӴ9VX8,6B!B!$۶ {9dL[-: P#P0M+ʘ$q@qs-(L!B!B!u`^`ZN!UXľJM$tؼ"Qc/ !B!B!`$d 1S/!B!B!cA_A!B!B!dTE>( !B!B!gkXCB!B!B!dQB!B!B>AhB!B!B! !B!B!&B!B!OȦi@!B!B!`c[̻ endstream endobj -1088 0 obj +1096 0 obj << /Filter /FlateDecode /Type /XObject @@ -32299,7 +32513,7 @@ x u^;uȀ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  o endstream endobj -1089 0 obj +1097 0 obj << /Type /XObject /Subtype /Image @@ -32314,7 +32528,7 @@ endobj /Columns 1920 /Colors 3 >> -/SMask 1090 0 R +/SMask 1098 0 R /Length 727652 >> stream @@ -34669,7 +34883,7 @@ AD Ȁ[Ů~ endstream endobj -1090 0 obj +1098 0 obj << /Filter /FlateDecode /Type /XObject @@ -34690,7 +34904,7 @@ x u^;uȀ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0. k endstream endobj -1091 0 obj +1099 0 obj << /Type /XObject /Subtype /Image @@ -34705,7 +34919,7 @@ endobj /Columns 1920 /Colors 3 >> -/SMask 1092 0 R +/SMask 1100 0 R /Length 287978 >> stream @@ -35699,7 +35913,7 @@ hH3 nHL4NR)Jee2Yw @`iiy> -/SMask 1096 0 R +/SMask 1104 0 R /Length 3917 >> stream @@ -36357,7 +36571,7 @@ x "wiNȌEFrסUUg~/tc5HDm;O-Xz%~0  "BA0m[fB)W? Cb.gQdw*q~`LsJf`P :*oW K[{i endstream endobj -1096 0 obj +1104 0 obj << /Filter /FlateDecode /Type /XObject @@ -36382,7 +36596,7 @@ f 9]Gd35 o_wX@l/7ۺae> endstream endobj -1097 0 obj +1105 0 obj << /Type /XObject /Subtype /Image @@ -36397,7 +36611,7 @@ endobj /Columns 100 /Colors 3 >> -/SMask 1098 0 R +/SMask 1106 0 R /Length 10160 >> stream @@ -36436,7 +36650,7 @@ U ߖM/#0RY5\YAo"zfHJn8t$E BReOӫD]mn~}U;v}$ ͠emq (brr2)d:q|Ҕ5^RB@OTW#(TD5[NW{w㳯hM(Eib&D "Ckݎ嵃ӱ*5P0QoaM|bZP{8͖c}a^cXՒsa}(V21jn{Ƙnʛԥi@v鏿5^>x&q m"eggǩynoNJʖx٧`'fCm,s˖R=;_k|"USM.Xĉf*&O_݅5£vfI=^ML<8TNڗ~p9kp endstream endobj -1098 0 obj +1106 0 obj << /Filter /FlateDecode /Type /XObject @@ -36458,7 +36672,7 @@ x 5gd9u ׊,H*UޑpJLj+𷘽*#C Rp*qw!Eml%?e~j!% C ,9a\G]h+1^v81B+W1/ʡ8T$~;#')Ȁq_N#ƚ=YH/#.8[Cb6 1b P,nSZ!;7 q%kH(z8 /i1vDtmKɊcD@` tBUZMm֗k iUvr&^@lYB~?Y)^hf{ps|tHgÖreUQ7tBcbNwoK1ꪀ=R| 0T1Pǽm8C' CbXaC ӈGC&L |Ug=ˀi01jgCs?#R]^FnO!ւd2l"##֙}~)Ue >2Ĵ-24-˲,3teAWsXttas4L KÃi0`MiI#nJ6OfOn%./W]` c8K~cVT&O7X4߹mWkh7TŶϞ> -/SMask 1100 0 R +/SMask 1108 0 R /Length 491902 >> stream @@ -38188,7 +38402,7 @@ BNN _i0d'p8p8뮽.%57TK&FHnnnS;vp8plDIYnp8p8$p8p8&p8p8GB4p8p8BVsp8p8YOpe endstream endobj -1100 0 obj +1108 0 obj << /Filter /FlateDecode /Type /XObject @@ -38208,7 +38422,7 @@ stream xԱ03^\Щs.ug|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||= endstream endobj -1101 0 obj +1109 0 obj << /Type /XObject /Subtype /Image @@ -38223,7 +38437,7 @@ endobj /Columns 1742 /Colors 3 >> -/SMask 1102 0 R +/SMask 1110 0 R /Length 172996 >> stream @@ -38654,7 +38868,7 @@ g%qg uRҀcO=7@^yT׼.p^V,?0Lz~j38MD_v4vmkþ%+ӥwշ&{,_^׌o}ʻ{Do I ^?(q!HoP79WfX7:3x[u2t|@[_s endstream endobj -1102 0 obj +1110 0 obj << /Filter /FlateDecode /Type /XObject @@ -38782,7 +38996,7 @@ v[t; ӺND[ Š +u~{O9]>+9xKl>'b{p:xc|~O=SDMnyw+znp:jǫ9-d1.m!Qq&;L^~د3R?p-!;?^ݷa~qJkx!H;' :Nc] endstream endobj -1103 0 obj +1111 0 obj << /Type /XObject /Subtype /Image @@ -38797,7 +39011,7 @@ endobj /Columns 1811 /Colors 3 >> -/SMask 1104 0 R +/SMask 1112 0 R /Length 622269 >> stream @@ -40874,7 +41088,7 @@ Q YdׇiU'XT7=]QYdEYdEYdEYϢO>֯j-",bKSv endstream endobj -1104 0 obj +1112 0 obj << /Filter /FlateDecode /Type /XObject @@ -40894,7 +41108,7 @@ stream x1 pT~]gٯD'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" D'@<Ox" endstream endobj -1105 0 obj +1113 0 obj << /Type /XObject /Subtype /Image @@ -40909,7 +41123,7 @@ endobj /Columns 1807 /Colors 3 >> -/SMask 1106 0 R +/SMask 1114 0 R /Length 28962 >> stream @@ -41042,7 +41256,7 @@ _\ 9.6n{`a)%P7˴$$Y7}$X渲kb$ھ=h|> -/SMask 1108 0 R +/SMask 1116 0 R /Length 588 >> stream @@ -41085,7 +41299,7 @@ x ս=O\Rʃ Զ"詉ǏNM5&?L" bH((& bhl,ptJשTbni0pӶ+pC-C4ЉR_WsFÉR˗Vy-'3ĺeOTVV@wW rPPOΆCbqg߶u9ˇ$sgܜV.rVU$§yPʙ]Ӳ jw;@JOęmԓQ̶ a3jX=Z@h{'յ Q@$ϹUV+.ؾY1hfpf@Tfb'+WΆ;MOZW.\Q7 endstream endobj -1108 0 obj +1116 0 obj << /Filter /FlateDecode /Type /XObject @@ -41106,7 +41320,7 @@ x T3 endstream endobj -1109 0 obj +1117 0 obj << /Type /XObject /Subtype /Image @@ -41121,7 +41335,7 @@ endobj /Columns 176 /Colors 3 >> -/SMask 1110 0 R +/SMask 1118 0 R /Length 2907 >> stream @@ -41139,7 +41353,7 @@ H D cYר aR(XV"k CD aFODoIPswavRiw!2}>ٚ俛|~!}z= ֘/jߌBCn7-E!ZhAZ!/(U endstream endobj -1110 0 obj +1118 0 obj << /Filter /FlateDecode /Type /XObject @@ -41161,7 +41375,7 @@ k ki? endstream endobj -1111 0 obj +1119 0 obj << /Type /XObject /Subtype /Image @@ -41176,7 +41390,7 @@ endobj /Columns 107 /Colors 3 >> -/SMask 1112 0 R +/SMask 1120 0 R /Length 2501 >> stream @@ -41191,7 +41405,7 @@ _okȘ #O. |}[[ۍ4 ;oDr.iA0OCn?eS+z endstream endobj -1112 0 obj +1120 0 obj << /Filter /FlateDecode /Type /XObject @@ -41211,7 +41425,7 @@ stream x 0gmq뭘U.r\R1 endstream endobj -1113 0 obj +1121 0 obj << /Type /XObject /Subtype /Image @@ -41226,7 +41440,7 @@ endobj /Columns 720 /Colors 3 >> -/SMask 1114 0 R +/SMask 1122 0 R /Length 8063 >> stream @@ -41266,7 +41480,7 @@ P $;Y_sʓoZs0Χtk %)鈇_>0H"EÃxxĈ 58k7 UZcĦpk?6KOlߙ՜،ji3 Im8}M'EsGM_$~9%xڵma[ QҥNfBj3d%B ҚHT^V[34r0,35\R^a(ITu*MNSk^XHӐ"CJLIT\cHsF( ],o ³sf˖S׍E'}%k}.KʼndIrNU"^p8Ѽ{q]PMR̐@sQȐL%7+b1A!B썀\B;9upeX|nM oX݋8P endstream endobj -1114 0 obj +1122 0 obj << /Filter /FlateDecode /Type /XObject @@ -41286,7 +41500,7 @@ stream x 0u%$= :xФCbhR MI14)&ФCbhR MI14)&ФCbhR MI14)&ФCbhR.iG; endstream endobj -1115 0 obj +1123 0 obj << /Type /XObject /Subtype /Image @@ -41301,7 +41515,7 @@ endobj /Columns 308 /Colors 3 >> -/SMask 1116 0 R +/SMask 1124 0 R /Length 1809 >> stream @@ -41309,7 +41523,7 @@ x ''''''''''''j,a8xN7tlnfLM{N~got{e{89888888888888l9Wp*}&GCw,>hy~N.֯Cg-PJPJPJPJPJPJPJPJPJPJPJPJPVJ<\{!^/6n'}IzSŋ_d3ƶR qB)qB)qB)qB)qB)qB)qB)qB)qB)qB)qB)qB)[)wدpr߀p<'Gm%*|r%+NN(%N(%N(%N(%N(%N(%N(%N(%N(%N(%N(%N(%N(u R=QY8[7GoNNNwwɿ-ݞY 07Hs&~|ɗ .VJkۍHwRF5OLxdǏOnùh߿2^RRRRRRRRRRRRRJyLk{8=xc6vg?7[7^ '&*lJJJJJJJJJJJJJmx+e.Qxco\[)턓|0|zzN.oh:H7Hf.w쇓?{=Jpr/6 (9''''''''''''''V.5r$+enf\J9o??N N+3*Ԇ99888888888888~-|:'ekQį9oy$}- 6|Wʶ8/|Mٖi[^9RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRM?\?wp  *= endstream endobj -1116 0 obj +1124 0 obj << /Filter /FlateDecode /Type /XObject @@ -41329,7 +41543,7 @@ stream xA @g>"%{pkiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii:g endstream endobj -1117 0 obj +1125 0 obj << /Type /XObject /Subtype /Image @@ -41344,7 +41558,7 @@ endobj /Columns 308 /Colors 3 >> -/SMask 1118 0 R +/SMask 1126 0 R /Length 1403 >> stream @@ -41353,7 +41567,7 @@ x w0.}7\޼.NK߅˝ǓJJJJJJJJJJJJJJ> Z ˍ!}vvn+u92?-.wҫ̓JJJJJJJJJJJJJJL'ry!x-搾e{& Y.gӟp4_겋yrB)qB)qB)qB)qB)qB)qB)qB)qB)qB)qB)qB)qB)W)[}!*emQs锧ʓJJJJJJJJJJJJO' endstream endobj -1118 0 obj +1126 0 obj << /Filter /FlateDecode /Type /XObject @@ -41374,7 +41588,7 @@ x @@(u+yG8ΣWMH {kȯsyG------------X݌ @@@@@@@@@@@@|&-------------X,?m9\פIn#Z Z Z Z Z Z Z Z Z Z Z Z|Qَrl#Z Z Z Z Z Z Z Z Z Z Z Zpdg\Ф?}I D D D D D D D D D D D Dx endstream endobj -1119 0 obj +1127 0 obj << /Type /XObject /Subtype /Image @@ -41389,14 +41603,14 @@ endobj /Columns 308 /Colors 3 >> -/SMask 1120 0 R +/SMask 1128 0 R /Length 299 >> stream x1 Om_~Y  endstream endobj -1120 0 obj +1128 0 obj << /Filter /FlateDecode /Type /XObject @@ -41417,7 +41631,7 @@ x 1U&xivA`qv&}3hhhhhhhhhhhhcu#1+dтw9&.&D D D D D D D D D D D D D D D D D D D D D D D D D D 1]-x}40ihhhhhhhhhhhh[s~äceƤ[be-s@@@@@@@@@@@@4~ 5 endstream endobj -1121 0 obj +1129 0 obj << /Type /XObject /Subtype /Image @@ -41432,14 +41646,14 @@ endobj /Columns 308 /Colors 3 >> -/SMask 1122 0 R +/SMask 1130 0 R /Length 299 >> stream x1 Om_~Y  endstream endobj -1122 0 obj +1130 0 obj << /Filter /FlateDecode /Type /XObject @@ -41460,7 +41674,7 @@ x 0@Q[YKYKFe׹ec6;-------------羶 1 endstream endobj -1123 0 obj +1131 0 obj << /Type /XObject /Subtype /Image @@ -41475,14 +41689,14 @@ endobj /Columns 308 /Colors 3 >> -/SMask 1124 0 R +/SMask 1132 0 R /Length 299 >> stream x1 Om_~Y  endstream endobj -1124 0 obj +1132 0 obj << /Filter /FlateDecode /Type /XObject @@ -41503,7 +41717,7 @@ x @@(u+yG8ΣWMH {kȯsyG------------X݌ @@@@@@@@@@@@|&-------------X,?m9\פIn#Z Z Z Z Z Z Z Z Z Z Z Z|Qَrl#Z Z Z Z Z Z Z Z Z Z Z Zpdg\Ф?}I D D D D D D D D D D D Dx endstream endobj -1125 0 obj +1133 0 obj << /Type /XObject /Subtype /Image @@ -41518,7 +41732,7 @@ endobj /Columns 165 /Colors 3 >> -/SMask 1126 0 R +/SMask 1134 0 R /Length 2190 >> stream @@ -41530,7 +41744,7 @@ qH[ r6Dgnpc*6#=*Bcfkef]π+IȪ:">sY)?,(* w2y4'{E^?@ QEVQ7Oqs@e|.-.-.-.-.-* endstream endobj -1126 0 obj +1134 0 obj << /Filter /FlateDecode /Type /XObject @@ -41550,7 +41764,7 @@ stream xA 1gc -XWZ:-NKitZ:-NKitZ:G endstream endobj -1127 0 obj +1135 0 obj << /Type /XObject /Subtype /Image @@ -41565,7 +41779,7 @@ endobj /Columns 165 /Colors 3 >> -/SMask 1128 0 R +/SMask 1136 0 R /Length 2189 >> stream @@ -41583,7 +41797,7 @@ s Ǔ= V p )]"L7OqԟP滴P滴P滴P滴P滴?+ endstream endobj -1128 0 obj +1136 0 obj << /Filter /FlateDecode /Type /XObject @@ -41603,7 +41817,7 @@ stream xA 1gc -XWZ:-NKitZ:-NKitZ:G endstream endobj -1129 0 obj +1137 0 obj << /Type /XObject /Subtype /Image @@ -41618,14 +41832,14 @@ endobj /Columns 165 /Colors 3 >> -/SMask 1130 0 R +/SMask 1138 0 R /Length 276 >> stream x F\/H2YܠΕخlfI`xހX|oBY7 f!,䛅|o[f91׭t(SK|˖޸5ߓDɟsxdwb"w^{؏&KL9߅}]fFz{;9hori^:åf5Lw)%3] G EkoBY7 f!,䛅|or? endstream endobj -1130 0 obj +1138 0 obj << /Filter /FlateDecode /Type /XObject @@ -41645,7 +41859,7 @@ stream xA 1gc -XWZ:-NKitZ:-NKitZ:G endstream endobj -1131 0 obj +1139 0 obj << /Type /XObject /Subtype /Image @@ -41660,7 +41874,7 @@ endobj /Columns 330 /Colors 3 >> -/SMask 1132 0 R +/SMask 1140 0 R /Length 2822 >> stream @@ -41675,7 +41889,7 @@ mN} dEG endstream endobj -1132 0 obj +1140 0 obj << /Filter /FlateDecode /Type /XObject @@ -41695,7 +41909,7 @@ stream xICAQ~Q"XC! ^N_EE\o>a1;aVfX2ceʌ+3VfX2ceʌ+3VfX2ceʌ+3VfX2ceʌ+3VfX2ceʌ+3VfX2ceʌ+3VfX2ceʌ+3VfX2ceʌ]xݰs<;b 1 endstream endobj -1133 0 obj +1141 0 obj << /Type /XObject /Subtype /Image @@ -41710,14 +41924,14 @@ endobj /Columns 330 /Colors 3 >> -/SMask 1134 0 R +/SMask 1142 0 R /Length 691 >> stream xn0eRm>yꃍl߀DnpX b7o%K!xC,X b7o%K!xC,X b7og۶^|/[`+=a۶KWūwPii{jCJɯߌmO֫jHImfwOO[?ʖt=C5/pY}YS~ދv#f84 >lGx8 Éچmߟ1kݞwUf܋zi瞭.i[/Tx=_|yG_ !ejGX8CHJ$<,jv}߫õ{Su4Ohzuƛ%5T]BT 4N!xC,X b7o%K!xC,X b7o%K!xC,X b7SN endstream endobj -1134 0 obj +1142 0 obj << /Filter /FlateDecode /Type /XObject @@ -41737,7 +41951,7 @@ stream x @@c{:VfX2ceʌ+3VfX2ceʌ+3VfX2ceʌ+3VfX2ceʌ+3VfX2ceʌ+3VfX2ceʌ+3VfX2ceʌ+3VfX2ceʌ+3VfX2ceL endstream endobj -1135 0 obj +1143 0 obj << /Type /XObject /Subtype /Image @@ -41752,7 +41966,7 @@ endobj /Columns 101 /Colors 3 >> -/SMask 1136 0 R +/SMask 1144 0 R /Length 2503 >> stream @@ -41770,7 +41984,7 @@ z J#DBY--A=f8Z704r!VL"JDCH [!p҃x:l/JFBRBR:DLZ2"R؜/Iixp$vDV4ig/exC.h$8`CNћt?e'JtǓ_1՚ﭳHJtߺ4]#+yI;'d_JWa_ٞuxi3d=+ߡ19./ecVn졛/{7fSm endstream endobj -1136 0 obj +1144 0 obj << /Filter /FlateDecode /Type /XObject @@ -41791,7 +42005,7 @@ x &X,bX,a9\QG endstream endobj -1137 0 obj +1145 0 obj << /Type /XObject /Subtype /Image @@ -41806,14 +42020,14 @@ endobj /Columns 2020 /Colors 3 >> -/SMask 1138 0 R +/SMask 1146 0 R /Length 10241 >> stream x{u}~eA*&E 8NG.E4U)`l4v՘hh`hBo$/Hdw;dg/_s{;SA` L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z L0z LQ`0A::MB {Dy{va^w/ L0z L0z L0z L0z L0º~T߰O>}駗a+/yEy{o,ok_/ok8)Cs;y% _ryϔ_+oW޶\ gc3[wvaamXޞ{ޅ1OU(onU޶}_>_޾ەתo[ +ory/|-GyDy;eii6gTz~^rɫ۟?+o۾;&@Xab=&@Xab=&@Xab=&@Xab=&@Xaa2]>:؍ 'p FlFay;;;{[#5W{]A7ln>' .+oj|C{vry{']ͽYov?TZ>6=ΔK.)o_n۾&_@Xab=&@Xab=&@Xab=&@Xab=&@Xab=/OoөsHyiyhX޶/E N;\וW\qyymO=Uv]ڱ}gyP~O__>)ow]޶箫?`.ryecϾ}.oWVVۖϔ; 7FŰ+5W׿yY}7/o֖mV뮽ᆷ Snۺu)&@Xab=&@Xab=&@Xab=&@Xab=&@(}ui-6Ghx[l۶#-O˜o~^wiy5f\y{QG/ow]}ںm[yiy7~]Z^.oay;=]䰶T?'ߗw; ʸϣ6_X޾,oo-&/;;[~/7=엔_ۻhyWy]^Y)oO L0z L0z L0z L0z LQ`dhxVЋեޤO~/[޶ڏX_ʫ,o??*ow[߿X~÷_Ty;7tkp8,O./v<V޾mח~yq|y ./Ǭ/ove~n G7ly{˭ ۮ]_Nuox6]/^~wSrm헿_w-7nPc&_@Xab=&@Xab=&@Xab=&@Xab=&@Xab=~U=l.]e&j9{ԷSm=wG\rqya~}l+o[Ϋ]u4ԟopbM۳>=*oۘԗsRy]~v8_ Mr¦3~귖_xau^^GZ9vϞvmxpl;z L0z L0z L0z L0z LQb0SO-o7/zQy;33Sޞw޹veܗ_={-o[ɛnzwya~}n.o<_͛7~Mqyac}/^_?Xعm1;WםyQGU޶ɣ9⊷'xry{-+o:][җT~wۖ{I'ύ^rvyۦ~nL/{UW^Qn/oyvye/y;33[N Q˷zj?Uޞpo}g=vv]ezTo-oۖ* j4Y\\*oow.o.~_@Xab=&@Xab=&@Xab=&@Xab=&@Xab=O߰pX޾u]Ë533]N_~]Z^)o͛ݳv8?կzuy{'S{_}1w] x\<[??/o3Iwy{y{)yXۿ㎏6u|.{vi@ypkWm"[ߎWwz};}v_B^wmm=blyu^Z\.o~f{yu؍T>9?Ly>|n;OMٳ_xSy{z L0z L0z L0z L0z xJG ۮaG]S~]yZWB7-Ϸ[(o~^p^y;f}?/o[ޣA)y h<_GKg-o۾s 1s3m;JyTٻm?7i3۷m/o]Y3Wޮ?~ _EuK x\ohr!-c^Ty;jx] ]vvE*o"MܱmM{0z L0z L0z L0z L0z7>C/oAwd_Tܲm9m:ZnRn}pm^æ[lХ0h>9n;)mB)@ w]mk>$Fz L0z L0z L0z L0z l>OݠKOo<߆mcny7A1ߖmy'.58Q}:oo'Ԉ} }vMֵ|\y9h/a_@Xab=&@Xab=&@Xab=&@Xab=&@Xab=]˸?XG1!Rw ZM]5ܯjONUw*n:Sj~[.Uw <ϓNv&_@Xab=&@Xab=&@Xab=&@Xab=&@Xab=t}wC] jZC/ L0z L0z L0z L0z L0Fn8vuxpe=&@Xab=&@Xab=&@Xab=&@Xab=&@(}ѷ<,b~Yab=&@Xab=&@Xab=&@Xab=&@Xab=6Jct-`pe=&@Xab=&@Xab=&@Xab=&@Xab=&@Xab=&@Xab=&@Xab=&@Xab=&@Xab=&@Xab=&@Xab=&@Xab=&@Xab=&@Xab=&@Xab=&@Xab=&@Xab=&@Xab=&@Xab=&@Xab=&@Xab=&@Xab=&@Xab=&@Xab=&@9Y endstream endobj -1138 0 obj +1146 0 obj << /Filter /FlateDecode /Type /XObject @@ -41833,7 +42047,7 @@ stream xA 0C!YZm ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((( r endstream endobj -1139 0 obj +1147 0 obj << /Type /XObject /Subtype /Image @@ -41848,14 +42062,14 @@ endobj /Columns 101 /Colors 3 >> -/SMask 1140 0 R +/SMask 1148 0 R /Length 239 >> stream xI ej*$SDJl%}ǩ) ^xO#FT鵆p Ă^a{sm+3_lWĚKD\tnT|:v!wا.{Urgp6^ZVѲ׫`ųEwG]mx/_X[L_8/_Y5Z٪t(њ7#?3 endstream endobj -1140 0 obj +1148 0 obj << /Filter /FlateDecode /Type /XObject @@ -41876,7 +42090,7 @@ x &X,bX,a9\QG endstream endobj -1141 0 obj +1149 0 obj << /Type /XObject /Subtype /Image @@ -41891,7 +42105,7 @@ endobj /Columns 202 /Colors 3 >> -/SMask 1142 0 R +/SMask 1150 0 R /Length 3532 >> stream @@ -41912,7 +42126,7 @@ c _L[.-@I]h,2 8ai=?LHۃE80jC @ @ @ @ @ @ /;; endstream endobj -1142 0 obj +1150 0 obj << /Filter /FlateDecode /Type /XObject @@ -41941,7 +42155,7 @@ fN +DBt6 H{. }W߷˧kի endstream endobj -1143 0 obj +1151 0 obj << /Type /XObject /Subtype /Image @@ -41956,7 +42170,7 @@ endobj /Columns 202 /Colors 3 >> -/SMask 1144 0 R +/SMask 1152 0 R /Length 547 >> stream @@ -41965,7 +42179,7 @@ x 㮄vq_ 8ڰ{ٍv7Æe4 =-d&~;u菡̠*DV%bIoiY2h`K^v{8ª1~qi$2R endstream endobj -1144 0 obj +1152 0 obj << /Filter /FlateDecode /Type /XObject @@ -41985,7 +42199,7 @@ stream x 0g-!8K=~^ӊQ+VDZ"jEԊQ+VDZ"jEԊQ+VDZ"jEԊQ+VDZ"jEԊQ+* endstream endobj -1145 0 obj +1153 0 obj << /Type /XObject /Subtype /Image @@ -42000,7 +42214,7 @@ endobj /Columns 202 /Colors 3 >> -/SMask 1146 0 R +/SMask 1154 0 R /Length 664 >> stream @@ -42009,7 +42223,7 @@ x ^ R\԰Qߤrk/v!A7j{=ޞS+˧֪KUVVb5&G ?w'b;|s?=m'ko^m/ݟXr'o_-^rKXv`> -/SMask 1148 0 R +/SMask 1156 0 R /Length 511942 >> stream @@ -43712,7 +43926,7 @@ f 3B!f!B!#*B!BGT!B!ޏ0C!B!/ endstream endobj -1148 0 obj +1156 0 obj << /Filter /FlateDecode /Type /XObject @@ -43820,7 +44034,7 @@ h-΀ Yr*&}l|'~وqmGe9ji }:MJefh nqnOLj$'U[ކ,L1QexV >[2xOmү0&tϸ:v|zv8iV?nVy≄jL/$aD=g=!z(jongtEk4ΤucME Rl̘?Z <7XGKYĔ:;ŷiə$,_ƞf ٨M#.ۀN~kMTLβ1v,4nzq=auINi9bM;ȑ0X;ƌa3w0z,N-hZ)'n8_>2Rb&*e=[Z̀ ZrY_v(8I楍|1'32sF;Z2 pH|$UҰ[eh LDŽ/%9OHwf99V3b,ն-M h!:.dM&$ɟx3uFӝCtFL6 ' aDXK<-(fCkj+---$upDƻ8ۭL6e N{tV&,%eYA!AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAb,@ endstream endobj -1149 0 obj +1157 0 obj << /Type /XObject /Subtype /Image @@ -43835,7 +44049,7 @@ endobj /Columns 727 /Colors 3 >> -/SMask 1150 0 R +/SMask 1158 0 R /Length 8085 >> stream @@ -43865,7 +44079,7 @@ Y 7Gd"qߍPuu+bԬ# !bs-)ziy,aqJvVb{ c4"U%b\(seXVO̊a\*m>E^}岌?%ػ 7Ҵ;&9<-9uO-qbtwncn!P% @79glb"P#^کQ1Z`@D]̂ QjKU A\3z/ t=܁3 ubw2:@Fvw/,SSMBã endstream endobj -1150 0 obj +1158 0 obj << /Filter /FlateDecode /Type /XObject @@ -43885,7 +44099,7 @@ stream xA 0C!YZ{lς;5E"_Sk|M)5E"_Sk|M)5E"_Sk|M)5E S; endstream endobj -1151 0 obj +1159 0 obj << /Type /XObject /Subtype /Image @@ -43900,7 +44114,7 @@ endobj /Columns 721 /Colors 3 >> -/SMask 1152 0 R +/SMask 1160 0 R /Length 13952 >> stream @@ -43951,7 +44165,7 @@ h'2 lE~*&Ƌq["^PWī6F{Ɔ5E`Q8&cy|w2h-GMxbR\mLM5֋Vel鞛]cC_onl0؝G*jz5> -/SMask 1154 0 R +/SMask 1162 0 R /Length 12198 >> stream @@ -44021,7 +44235,7 @@ J ұ0d*D+HJQ;tbeqn~bbOOUӠ t͟ƛ8n{]#&䀅03{fmCd7qޯ"n&sI4A5kjHL0XkBqxQ=hkvj$Z%Z*BeK 3m72ǽo+kf:{N^LL uj״=3۟֗X'0:E0si2,fҪ"(υGi܈q<.3er$,YOʬjy ^Kc+sn5VMHܛE/lඓd͹GLO=\L>yZ̜u\vf66}{"d#Oa͸I49{{f!Dᔟm(%HW2WZLշ0~}|4Q)3YlÎ>"?:5M vǸ%HmuZ؂Utfձ}Qoa-o0/usAS&`G!s5Ac% LkPV  SS`1%1CTؒYQ.&|%%64qԦ ,3Vl`}!)="KQ_ļجO"f,kO':*,dubYƈ/>+sr-grN˚^<ti\O'o,{'LZ& 0)P=EQ/SN |ayN $zQj{ 9;a|'Y.OM/b3 endstream endobj -1154 0 obj +1162 0 obj << /Filter /FlateDecode /Type /XObject @@ -44041,7 +44255,7 @@ stream x 0u%$= jxٚ [dklM 5A&ٚ [dklM 5A&ٚ [dklM 5A&ٚ [dk.2n; endstream endobj -1155 0 obj +1163 0 obj << /Type /XObject /Subtype /Image @@ -44056,7 +44270,7 @@ endobj /Columns 731 /Colors 3 >> -/SMask 1156 0 R +/SMask 1164 0 R /Length 8276 >> stream @@ -44086,7 +44300,7 @@ m a*^/oV!6Ī A8kh++NmeJ#k,xͿH\ڷ̫a̅t^ΕCU +|]DwZůjwMVݱZvmz5BFF\簯K6G~Pul_#@HkQ)rg+%^F?&q endstream endobj -1156 0 obj +1164 0 obj << /Filter /FlateDecode /Type /XObject @@ -44106,7 +44320,7 @@ stream xA 0C!YZ{lς;6UަTy*oSmM6UަTy*oSmM6UަTy*oSmM6UަTy*oSm.? endstream endobj -1157 0 obj +1165 0 obj << /Type /XObject /Subtype /Image @@ -44121,7 +44335,7 @@ endobj /Columns 717 /Colors 3 >> -/SMask 1158 0 R +/SMask 1166 0 R /Length 7952 >> stream @@ -44148,7 +44362,7 @@ b |cfbFʼ@~NmT82VWC+wvp^9AD"6n֪kB_En&SwlܜLBp~lh! j |W00)L0'I·j|BvjO[9x)L|wZ]uFuM|#KxQ> -/SMask 1160 0 R +/SMask 1168 0 R /Length 1322 >> stream @@ -44195,7 +44409,7 @@ F ؐ6jF :V/a(f$ov`g7VF0X?ݙ/O}㻽[ҰxӸ#i˧ͧ+g 9q@0S_~Z^~I}4L|MGxo7{{:I3e@MZ xѼ}XLG@dgvf 7nwXI"9<ڣ:wv%\ 爈_!oe endstream endobj -1160 0 obj +1168 0 obj << /Filter /FlateDecode /Type /XObject @@ -44215,174 +44429,6 @@ stream xcπ0- endstream endobj -1161 0 obj -<< -/Type /XObject -/Subtype /Image -/Width 200 -/Height 100 -/ColorSpace /DeviceRGB -/BitsPerComponent 8 -/Interpolate true -/Filter /FlateDecode -/DecodeParms << -/Predictor 15 -/Columns 200 -/Colors 3 ->> -/SMask 1162 0 R -/Length 279 ->> -stream -xA @g-.{}u2 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c8 -endstream -endobj -1162 0 obj -<< -/Filter /FlateDecode -/Type /XObject -/Subtype /Image -/DecodeParms << -/Predictor 15 -/Columns 200 ->> -/Width 200 -/Height 100 -/ColorSpace /DeviceGray -/BitsPerComponent 8 -/Interpolate true -/Length 137 ->> -stream -x 0g-!8K=^҈M#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈ4 -endstream -endobj -1163 0 obj -<< -/Type /XObject -/Subtype /Image -/Width 200 -/Height 100 -/ColorSpace /DeviceRGB -/BitsPerComponent 8 -/Interpolate true -/Filter /FlateDecode -/DecodeParms << -/Predictor 15 -/Columns 200 -/Colors 3 ->> -/SMask 1164 0 R -/Length 283 ->> -stream -xA @'TNAgz?HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH\| -endstream -endobj -1164 0 obj -<< -/Filter /FlateDecode -/Type /XObject -/Subtype /Image -/DecodeParms << -/Predictor 15 -/Columns 200 ->> -/Width 200 -/Height 100 -/ColorSpace /DeviceGray -/BitsPerComponent 8 -/Interpolate true -/Length 137 ->> -stream -x 0g-!8K=^҈M#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈ4 -endstream -endobj -1165 0 obj -<< -/Type /XObject -/Subtype /Image -/Width 200 -/Height 100 -/ColorSpace /DeviceRGB -/BitsPerComponent 8 -/Interpolate true -/Filter /FlateDecode -/DecodeParms << -/Predictor 15 -/Columns 200 -/Colors 3 ->> -/SMask 1166 0 R -/Length 284 ->> -stream -x @u1*8fhyd> ں?HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH -endstream -endobj -1166 0 obj -<< -/Filter /FlateDecode -/Type /XObject -/Subtype /Image -/DecodeParms << -/Predictor 15 -/Columns 200 ->> -/Width 200 -/Height 100 -/ColorSpace /DeviceGray -/BitsPerComponent 8 -/Interpolate true -/Length 137 ->> -stream -x 0g-!8K=^҈M#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈ4 -endstream -endobj -1167 0 obj -<< -/Type /XObject -/Subtype /Image -/Width 200 -/Height 100 -/ColorSpace /DeviceRGB -/BitsPerComponent 8 -/Interpolate true -/Filter /FlateDecode -/DecodeParms << -/Predictor 15 -/Columns 200 -/Colors 3 ->> -/SMask 1168 0 R -/Length 283 ->> -stream -xA @ T#kHȝ>:^M"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,B -endstream -endobj -1168 0 obj -<< -/Filter /FlateDecode -/Type /XObject -/Subtype /Image -/DecodeParms << -/Predictor 15 -/Columns 200 ->> -/Width 200 -/Height 100 -/ColorSpace /DeviceGray -/BitsPerComponent 8 -/Interpolate true -/Length 137 ->> -stream -x 0g-!8K=^҈M#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈ4 -endstream -endobj 1169 0 obj << /Type /XObject @@ -44399,10 +44445,10 @@ endobj /Colors 3 >> /SMask 1170 0 R -/Length 285 +/Length 279 >> stream -xA @l3dSGπHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH  +xA @g-.{}u2 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c8 endstream endobj 1170 0 obj @@ -44429,27 +44475,22 @@ endobj << /Type /XObject /Subtype /Image -/Width 27 -/Height 23 +/Width 200 +/Height 100 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Interpolate true /Filter /FlateDecode /DecodeParms << /Predictor 15 -/Columns 27 +/Columns 200 /Colors 3 >> /SMask 1172 0 R -/Length 1259 +/Length 283 >> stream -xuU[oG>gfwM;&@hB{DiAJD*% K[UjEF*P U -!ic;%L6,IGٙ|;ߜE8! Bt&]*4MDBP"|J,hv}'|ܽApHbSўt*sw|yوԩO5M?w܃SzsfsRGE3< us '?׫?c6""PJuD>}QO"/9#C8:z4iH0ΙaxouȻd!I@)!p*tG;;~p~I\ӍW`mM7ɋ{]]oG&c٥{b3&3B--ÆTMZnL٣u8$.mSSKˌ1[[܊Fattthh7%Ė?@ -rti^hH  (S&'Li.vwN'$C;{rgjmM UiQUUu݊$IHp}b(J|~]$ +++hfZBUU75VWVJO]J4|-T*-//3#$jZcSVDbL5 hZ쁜7og2i8"rww_9 -h!Y8N1Lq4 M4 洈rܹ\3`K׫nf.2fckbV[a 8gujCrkmk - -gҩrlo>999`0M@i=+ s]LP@\T*X"g@ŊKEKR\iP0А(s{"D"_h44Ȍ_<8ݵ)IV?K:Rg\~##""ΙaWrc>ڱ,SJ+lxKY퍎JB) DTU;q{{K߿JgMm@) v]\L}ץ2"$8G/bP}{ dǏgd>jaBnz> -/Width 27 -/Height 23 +/Width 200 +/Height 100 /ColorSpace /DeviceGray /BitsPerComponent 8 /Interpolate true -/Length 22 +/Length 137 >> stream -xcπ0- +x 0g-!8K=^҈M#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈ4 endstream endobj 1173 0 obj @@ -44518,6 +44559,90 @@ endobj << /Type /XObject /Subtype /Image +/Width 200 +/Height 100 +/ColorSpace /DeviceRGB +/BitsPerComponent 8 +/Interpolate true +/Filter /FlateDecode +/DecodeParms << +/Predictor 15 +/Columns 200 +/Colors 3 +>> +/SMask 1176 0 R +/Length 283 +>> +stream +xA @ T#kHȝ>:^M"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,"a,B +endstream +endobj +1176 0 obj +<< +/Filter /FlateDecode +/Type /XObject +/Subtype /Image +/DecodeParms << +/Predictor 15 +/Columns 200 +>> +/Width 200 +/Height 100 +/ColorSpace /DeviceGray +/BitsPerComponent 8 +/Interpolate true +/Length 137 +>> +stream +x 0g-!8K=^҈M#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈ4 +endstream +endobj +1177 0 obj +<< +/Type /XObject +/Subtype /Image +/Width 200 +/Height 100 +/ColorSpace /DeviceRGB +/BitsPerComponent 8 +/Interpolate true +/Filter /FlateDecode +/DecodeParms << +/Predictor 15 +/Columns 200 +/Colors 3 +>> +/SMask 1178 0 R +/Length 285 +>> +stream +xA @l3dSGπHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH  +endstream +endobj +1178 0 obj +<< +/Filter /FlateDecode +/Type /XObject +/Subtype /Image +/DecodeParms << +/Predictor 15 +/Columns 200 +>> +/Width 200 +/Height 100 +/ColorSpace /DeviceGray +/BitsPerComponent 8 +/Interpolate true +/Length 137 +>> +stream +x 0g-!8K=^҈M#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈ4 +endstream +endobj +1179 0 obj +<< +/Type /XObject +/Subtype /Image /Width 27 /Height 23 /ColorSpace /DeviceRGB @@ -44529,7 +44654,96 @@ endobj /Columns 27 /Colors 3 >> -/SMask 1176 0 R +/SMask 1180 0 R +/Length 1259 +>> +stream +xuU[oG>gfwM;&@hB{DiAJD*% K[UjEF*P U +!ic;%L6,IGٙ|;ߜE8! Bt&]*4MDBP"|J,hv}'|ܽApHbSўt*sw|yوԩO5M?w܃SzsfsRGE3< us '?׫?c6""PJuD>}QO"/9#C8:z4iH0ΙaxouȻd!I@)!p*tG;;~p~I\ӍW`mM7ɋ{]]oG&c٥{b3&3B--ÆTMZnL٣u8$.mSSKˌ1[[܊Fattthh7%Ė?@ +rti^hH  (S&'Li.vwN'$C;{rgjmM UiQUUu݊$IHp}b(J|~]$ +++hfZBUU75VWVJO]J4|-T*-//3#$jZcSVDbL5 hZ쁜7og2i8"rww_9 +h!Y8N1Lq4 M4 洈rܹ\3`K׫nf.2fckbV[a 8gujCrkmk + +gҩrlo>999`0M@i=+ s]LP@\T*X"g@ŊKEKR\iP0А(s{"D"_h44Ȍ_<8ݵ)IV?K:Rg\~##""ΙaWrc>ڱ,SJ+lxKY퍎JB) DTU;q{{K߿JgMm@) v]\L}ץ2"$8G/bP}{ dǏgd>jaBnz> +/Width 27 +/Height 23 +/ColorSpace /DeviceGray +/BitsPerComponent 8 +/Interpolate true +/Length 22 +>> +stream +xcπ0- +endstream +endobj +1181 0 obj +<< +/Type /XObject +/Subtype /Image +/Width 200 +/Height 100 +/ColorSpace /DeviceRGB +/BitsPerComponent 8 +/Interpolate true +/Filter /FlateDecode +/DecodeParms << +/Predictor 15 +/Columns 200 +/Colors 3 +>> +/SMask 1182 0 R +/Length 284 +>> +stream +x @u1*8fhyd> ں?HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH +endstream +endobj +1182 0 obj +<< +/Filter /FlateDecode +/Type /XObject +/Subtype /Image +/DecodeParms << +/Predictor 15 +/Columns 200 +>> +/Width 200 +/Height 100 +/ColorSpace /DeviceGray +/BitsPerComponent 8 +/Interpolate true +/Length 137 +>> +stream +x 0g-!8K=^҈M#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈ4 +endstream +endobj +1183 0 obj +<< +/Type /XObject +/Subtype /Image +/Width 27 +/Height 23 +/ColorSpace /DeviceRGB +/BitsPerComponent 8 +/Interpolate true +/Filter /FlateDecode +/DecodeParms << +/Predictor 15 +/Columns 27 +/Colors 3 +>> +/SMask 1184 0 R /Length 1039 >> stream @@ -44544,7 +44758,7 @@ y (tKZD4R7x<e [!nH1 tnrܙcڱtPU1R(q D9&9л|͋w;5YVASO+6υ|GOJ endstream endobj -1176 0 obj +1184 0 obj << /Filter /FlateDecode /Type /XObject @@ -44564,7 +44778,7 @@ stream xcπ0- endstream endobj -1177 0 obj +1185 0 obj << /Type /XObject /Subtype /Image @@ -44579,7 +44793,7 @@ endobj /Columns 27 /Colors 3 >> -/SMask 1178 0 R +/SMask 1186 0 R /Length 298 >> stream @@ -44588,7 +44802,7 @@ x Pⶨ7jMd"6:5*}W_wYK%m*b$A2Clh{:K7잿iiO endstream endobj -1178 0 obj +1186 0 obj << /Filter /FlateDecode /Type /XObject @@ -44608,7 +44822,7 @@ stream xcπ0- endstream endobj -1179 0 obj +1187 0 obj << /Type /XObject /Subtype /Image @@ -44623,7 +44837,7 @@ endobj /Columns 108 /Colors 3 >> -/SMask 1180 0 R +/SMask 1188 0 R /Length 3629 >> stream @@ -44639,7 +44853,7 @@ C *fwyj2ZC")+ݍsk;ۼ;ypKw)xHrRa&4'{/g??~ +-Jye=X6Kb @,A%hm X6Kb @,A/DJTbiJ=oT=SyJ,m~m. 3F endstream endobj -1180 0 obj +1188 0 obj << /Filter /FlateDecode /Type /XObject @@ -44660,7 +44874,7 @@ x W)-`y>n>k y0 endstream endobj -1181 0 obj +1189 0 obj << /Type /XObject /Subtype /Image @@ -44675,14 +44889,14 @@ endobj /Columns 108 /Colors 3 >> -/SMask 1182 0 R +/SMask 1190 0 R /Length 389 >> stream xN0@Q1/[Tzx¥x<^xE("PD@"E("PD@"E("PDm珏XUyoJ\,OfZ4t;O|ǓO(9A;xѳnNv~;?}}>];8?ϥӼ?v\/ cE("PD@"E("PD@"E("PD@ #~]s}uU=caLε¬=^,.8s#feɹu;&]arE+ ("PD@"E("PD@"E("PD@"EK endstream endobj -1182 0 obj +1190 0 obj << /Filter /FlateDecode /Type /XObject @@ -44703,7 +44917,7 @@ x [9d2L&d2L&d2L&d2L&d2L&{A endstream endobj -1183 0 obj +1191 0 obj << /Type /XObject /Subtype /Image @@ -47088,7 +47302,7 @@ Du ֤aaaaaaa;k 0 0 0 0 0 0 0̝5iaaaaaaaN40 0 0 0 0 0 0 s`MaaaaaaaS&0 0 0 0 0 0 0 )Xfaaaaaaa endstream endobj -1184 0 obj +1192 0 obj << /Type /XObject /Subtype /Image @@ -47113,7 +47327,7 @@ x cfkt pYJ~Q*,krjSueLͦfNJ'Ri\L?=y.gmf0E藒}h>ycd1(?-墇<}(!<=[JoBwyx.mZ2T>pQljLJd)띮`S? Χr)F_Z,Bv}m>vsQ 2;<+>/I;y'<B!C!D"x<B!C!D2}~w̷_E9!D"x<B!C!D"x<B!<:~{/}"x<B!C!D"x<B!Cy<ܗyo"x<B!C!D"x<B!C!D"x<B!C!D"x<B!CsY_;'<B!C!D"x<B!C!Dr𳙇Vm^}6'<B!C!D"x<B!C!D>~i|sC!D"x<B!C!D"x<Bxs\# endstream endobj -1187 0 obj +1195 0 obj << /Type /XObject /Subtype /Image @@ -47637,7 +47851,7 @@ c B ;3z ҿm }nNwz}n u;/ۖ> -/SMask 1189 0 R +/SMask 1197 0 R /Length 274159 >> stream @@ -48591,7 +48805,7 @@ A O|$ݑ;A'c[q(%+k9j,pL73Ҁcef>2Cq@~=B2"=g,8i3T]zIMtUL` dIZq'6pED nڽ',dfH") (Qh0rY% endstream endobj -1189 0 obj +1197 0 obj << /Filter /FlateDecode /Type /XObject @@ -48611,7 +48825,7 @@ stream x1 0础^ݳh:xh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh,Ȳh /y endstream endobj -1190 0 obj +1198 0 obj << /Type /XObject /Subtype /Image @@ -48626,7 +48840,7 @@ endobj /Columns 1901 /Colors 3 >> -/SMask 1191 0 R +/SMask 1199 0 R /Length 117846 >> stream @@ -48952,7 +49166,7 @@ O^ ݣ$J$-t^Cl;~yc_쪦֬zb U(;ס?73nÂ͛:LxwSSS3F1t=Ì?3q#?-]fo?ýf=*pC;,Ќ O@BßiϘsHρ<ׄ߾u =<'~K?UQQӷlw+n9쿯8|Og&i% endstream endobj -1191 0 obj +1199 0 obj << /Filter /FlateDecode /Type /XObject @@ -48972,7 +49186,7 @@ stream xA 0CBBZmRu|mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mPs[-jn 5@mP7$G endstream endobj -1192 0 obj +1200 0 obj << /Type /XObject /Subtype /Image @@ -52033,7 +52247,7 @@ M wNL@*R_E6A#i2jNhj?7|E(ShGj?((oU&E5iۨizAZ(f[#S@ HѻmMnZoPM}֢E ERR?ޠQMA#mnBRfԛjm:m6ڂrI4P endstream endobj -1193 0 obj +1201 0 obj << /Type /XObject /Subtype /Image @@ -55706,7 +55920,7 @@ P rj#=_ޡwlehv|z%^~emLcnҤnM۾*@̨&?~dҪŸ c?})i8Q4>jjfv[妏w}ij{f$ٹ7S~F6ݫmYmwTՠ E1 endstream endobj -1194 0 obj +1202 0 obj << /Type /XObject /Subtype /Image @@ -55721,7 +55935,7 @@ endobj /Columns 330 /Colors 3 >> -/SMask 1195 0 R +/SMask 1203 0 R /Length 11173 >> stream @@ -55750,7 +55964,7 @@ J,g} BZ^뺾WJaaZiJy^/Z$֦m[0:sZ[Ԙ{A1F!sԔ[ 6&ŠsI[ =?aOY摗R]/(-xl[l켚)|k ҉L#CHJ-,/&}JJ+%ONTbv:.  Ƃ1!$`WB_p:~^{kRrcF *sF&=RͱpoUB2 eXݴQ7S<d8~P]8Nb3ćl1śa@VB$MoN)j _>ɉ.JcpZW~ (Oc p@=z@aJDA\ ١FPTNeM. k! ݄nAᯆtL l2Bxc 0#ύ {7MT7Sqy}Xzys<<'!ohK{=WRb^}A;_o4c}Z|;?# 4b| 'o8|ZޏI}Ëw@l_8@ۑ S3{+[? CҟtQ^S?7墽/4y&rE>'?/\}29?E~ "?\\kO+x_"?\}u;%8 endstream endobj -1195 0 obj +1203 0 obj << /Filter /FlateDecode /Type /XObject @@ -55770,7 +55984,7 @@ stream x @@c{:VfX2ceʌ+3VfX2ceʌ+3VfX2ceʌ+3VfX2ceʌ+3VfX2ceʌ+3VfX2ceʌ+3VfX2ceʌ+3VfX2ceʌ+3Vf.R endstream endobj -1196 0 obj +1204 0 obj << /Type /XObject /Subtype /Image @@ -55785,7 +55999,7 @@ endobj /Columns 312 /Colors 3 >> -/SMask 1197 0 R +/SMask 1205 0 R /Length 6008 >> stream @@ -55816,7 +56030,7 @@ Q uL~/|{B"D!B"D!B"D!B"D!B ^ endstream endobj -1197 0 obj +1205 0 obj << /Filter /FlateDecode /Type /XObject @@ -55836,7 +56050,7 @@ stream xA 0>UlϢ8~e\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\tZ} endstream endobj -1198 0 obj +1206 0 obj << /Type /XObject /Subtype /Image @@ -55851,7 +56065,7 @@ endobj /Columns 270 /Colors 3 >> -/SMask 1199 0 R +/SMask 1207 0 R /Length 4691 >> stream @@ -55873,7 +56087,7 @@ S8 w?ݢA ?Km9ir S#p |>nzp]W`020p4~p ?{bh4J4FѣoA~Ȧ@,4hέᝀyO=X Bdgxi:`0F,0 SZ678{18{SKUa͠0{)GY a A,]T:+U9Iqk«ӧE΋"zM6 H$(^+А^JA E~nbw#Fu/8$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$_- endstream endobj -1199 0 obj +1207 0 obj << /Filter /FlateDecode /Type /XObject @@ -55893,7 +56107,7 @@ stream x1 0g8*X6ZbGvaGvaGvaGvaGvaGvaGvaGvaGvaGvaGvaGvaGvaGvaGjw endstream endobj -1200 0 obj +1208 0 obj << /Type /XObject /Subtype /Image @@ -55908,7 +56122,7 @@ endobj /Columns 773 /Colors 3 >> -/SMask 1201 0 R +/SMask 1209 0 R /Length 149970 >> stream @@ -56408,7 +56622,7 @@ kݺ U&5VE R5bU0]iحV+叏 t:;ٞ;{mqSd* 9oڥzӲzA;]s$ =2 J??͝lh> -/SMask 1203 0 R +/SMask 1211 0 R /Length 114393 >> stream @@ -56952,7 +57166,7 @@ w <'X EZ>,27?; B*""" @* B*""" @* B*""" @* B*""" @* B*""" @* B*""" @* B*""" @* B*""" @* B*""" @* B*""" @* B*""" @* B*""" @* B*""" @* B*""" @* "~\N&,˾|s^)%@*/bB|kRo޼A*;csj2|4}m6}1??<&ݝ endstream endobj -1203 0 obj +1211 0 obj << /Filter /FlateDecode /Type /XObject @@ -56972,7 +57186,7 @@ stream xA χ Hh5xP>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>T endstream endobj -1204 0 obj +1212 0 obj << /Type /XObject /Subtype /Image @@ -56987,7 +57201,7 @@ endobj /Columns 766 /Colors 3 >> -/SMask 1205 0 R +/SMask 1213 0 R /Length 121085 >> stream @@ -57377,7 +57591,7 @@ JI :ԚM}_ىӫqY*:gNZ8aE>ӂLcojs/5]Lz7z&M٢n 7rSs'!21!J) j3!1qaG3O1{^}宛g:!γ675eGPB -XA/J>k]q (óX#8g.&KJsXjkp}P\>䅟ݢ:VAKC/ۃ@>|JSJ"vf~\Ay{B%_4z_'\[=|>'O~[G_E??_Z?<;+'Z> 8c|=g)U 91NsZ\I9H k_kLJc7N#?+>=&&e^?/'}>镅1b!r$dZ_3W*}O+BՆPD}ޭL,>o+J>? 45Q>\ }"@pE_Gie{I]?k+Oy6 d /R +WH>\ }"@pEx!/ PUR N)>^>]?|nooyf׶E0?pu }޾?Ku]}͛?m۶w|_d۶7m:^TU|(s!R[/4/HmڶKP'WH>\ }"@pE+WH>\ }"@pE+WH>\ }"@pE+WH>\ }"@pE+WH>\ }"@pE+WH>\ }"@pE+WH>\ }"@pE+WH>\ }"@pE+WH>\ }"@pE+WH>\ }"@pE+WH>\ }"@pE+WH>\ }"@pE+W|ڶUJk_>B?3@)" }1&Xk^[ 6g>\ }" endstream endobj -1205 0 obj +1213 0 obj << /Filter /FlateDecode /Type /XObject @@ -57397,7 +57611,7 @@ stream x1 0!cG=z@='ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0fO ?a'ٟ0f>k endstream endobj -1206 0 obj +1214 0 obj << /Type /XObject /Subtype /Image @@ -57412,7 +57626,7 @@ endobj /Columns 901 /Colors 3 >> -/SMask 1207 0 R +/SMask 1215 0 R /Length 3790 >> stream @@ -57447,7 +57661,7 @@ x @&}L(Q2dGȤI d:}M.ޏI V endstream endobj -1207 0 obj +1215 0 obj << /Filter /FlateDecode /Type /XObject @@ -57467,7 +57681,7 @@ stream x1 0 }:[z. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. Bj. endstream endobj -1208 0 obj +1216 0 obj << /Type /XObject /Subtype /Image @@ -57482,7 +57696,7 @@ endobj /Columns 847 /Colors 3 >> -/SMask 1209 0 R +/SMask 1217 0 R /Length 5720 >> stream @@ -57499,7 +57713,7 @@ Sɛ/ ^[üRI 6k:ݏaP+ѭ[7J}k=†ݐgHLMh}{}n fL{1c@̘bƴ3=ѷ?QRSIUHw3O^R\}+7۾uʂ{Pͧz+w밷\*̌* |}b-)=1Či fL{1cЖ endstream endobj -1209 0 obj +1217 0 obj << /Filter /FlateDecode /Type /XObject @@ -57520,7 +57734,7 @@ x $= ?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOt?AOйzg endstream endobj -1210 0 obj +1218 0 obj << /Type /XObject /Subtype /Image @@ -57535,7 +57749,7 @@ endobj /Columns 770 /Colors 3 >> -/SMask 1211 0 R +/SMask 1219 0 R /Length 119052 >> stream @@ -58037,7 +58251,7 @@ V p4$<cy6 :==5i:OOJ>gà?_3^Iqjà?}r| GEeA@= _P~MP)aA8P@AaA8P@AaA8P@AaA8P@AaA8P@AaA8P@AaA8P@AaA8P@AaA8P@AaA8P@AaA8P@AaA8P@AaA8P@AaA8P@AaA8P@AaA8P@AaA8P@AaA8P@AaA8P@AaA8P@AaA8P@AaA8P@AaA8P@AaA8P@AaA8P@AaA8P@AaA8P@o}~C1*MS0c_W[ 8 0ƄzR~>oRp_o} /?q endstream endobj -1211 0 obj +1219 0 obj << /Filter /FlateDecode /Type /XObject @@ -58057,7 +58271,7 @@ stream x1 0!cG}z@] Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@& endstream endobj -1212 0 obj +1220 0 obj << /Type /XObject /Subtype /Image @@ -58072,7 +58286,7 @@ endobj /Columns 768 /Colors 3 >> -/SMask 1213 0 R +/SMask 1221 0 R /Length 108398 >> stream @@ -58567,7 +58781,7 @@ e2 >4Om߽n뺞N\t:=#t:f<Ғ|ZA6l6d2YO`P. S8 LtzH$~b8bəNs|||ttpWߢZvyI.˷:?aՃ. $ihT*n|FyF!K^yHRbtjSoŒL&Raw=#ouvv/p+{&BzMѨiӵZMӴɼlJ8Hig0DZF\Jeő'JlptIJ徭Rz%%>6B4zT*]Yj=Nsrr2fƖJ[& CPuY6o^Rpn&BVq&V[TyHד[2iI\rs)U'_f5M;?{_JLj)ɝRގ9̷Y$aK98?S!7>T<==@&wqkhffF-SB?w6dIla19(=C8g~D]cg>Ic}wr@@9 P(C!r@@9 P(C!r#^s8>o0[k8N:==ܫQhee믿pfu TvϷLjF~ޚg333evvb|jTvޚp?C!r@@9 P(C!r@@9 P(C!r@@9 P(C!r@@9 P(C!r@@9 P(C!r@@9 P(C!r@@9 P(C!r@@9 P(C!r@@9 P(C!r@@9 P(C!r@@9 P(C!r@@9 P(C!r@@9 P(C!r`iZ-g0D"^g2>v[I!gzzzffnJnLt\mWA{5g>~F0tUiZ&izzou_"n<Af endstream endobj -1213 0 obj +1221 0 obj << /Filter /FlateDecode /Type /XObject @@ -58587,7 +58801,7 @@ stream x1 0!cG}z@M 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i 4f @H3i  endstream endobj -1214 0 obj +1222 0 obj << /Type /XObject /Subtype /Image @@ -58602,7 +58816,7 @@ endobj /Columns 775 /Colors 3 >> -/SMask 1215 0 R +/SMask 1223 0 R /Length 104078 >> stream @@ -59055,7 +59269,7 @@ N@C {%$ -@nocx pE+(W0\@" WP`"rE+\pE+(W0\@" WP`"rE+\pE+(W0\@" WP`"rE+\pE+(W0\@" WP`"rE+\pE+(W0\@"}>|>zmsEa~O-q۶-u߯1npu]C endstream endobj -1215 0 obj +1223 0 obj << /Filter /FlateDecode /Type /XObject @@ -59075,7 +59289,7 @@ stream xA χ Hh5xP>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|(@P>|U? endstream endobj -1216 0 obj +1224 0 obj << /Type /XObject /Subtype /Image @@ -59090,7 +59304,7 @@ endobj /Columns 770 /Colors 3 >> -/SMask 1217 0 R +/SMask 1225 0 R /Length 104505 >> stream @@ -59508,7 +59722,7 @@ S \Px>gv 0M88k8ź˲RJ) endstream endobj -1217 0 obj +1225 0 obj << /Filter /FlateDecode /Type /XObject @@ -59528,7 +59742,7 @@ stream x1 0!cG}z@] Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q YJ{ endstream endobj -1218 0 obj +1226 0 obj << /Type /XObject /Subtype /Image @@ -59543,7 +59757,7 @@ endobj /Columns 770 /Colors 3 >> -/SMask 1219 0 R +/SMask 1227 0 R /Length 119685 >> stream @@ -60050,7 +60264,7 @@ A~ AA. *u?{-4M2A_99kA7Gk2A!J)cLk`A7G)E^  O- \((AD>| AA.'?/٫BAy_iQW| endstream endobj -1219 0 obj +1227 0 obj << /Filter /FlateDecode /Type /XObject @@ -60070,7 +60284,7 @@ stream x1 0!cG}z@] Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 g,@q Y8 0x endstream endobj -1220 0 obj +1228 0 obj << /Type /XObject /Subtype /Image @@ -60085,7 +60299,7 @@ endobj /Columns 767 /Colors 3 >> -/SMask 1221 0 R +/SMask 1229 0 R /Length 108864 >> stream @@ -60461,7 +60675,7 @@ D endstream endobj -1221 0 obj +1229 0 obj << /Filter /FlateDecode /Type /XObject @@ -60481,7 +60695,7 @@ stream x1 0!cG=z@E2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)?e2SO)2, endstream endobj -1222 0 obj +1230 0 obj << /Type /XObject /Subtype /Image @@ -60496,7 +60710,7 @@ endobj /Columns 777 /Colors 3 >> -/SMask 1223 0 R +/SMask 1231 0 R /Length 110124 >> stream @@ -60952,7 +61166,7 @@ c? )J_?SP?k endstream endobj -1223 0 obj +1231 0 obj << /Filter /FlateDecode /Type /XObject @@ -60972,7 +61186,7 @@ stream x10axtS% ;('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8 0Nr('8> endstream endobj -1224 0 obj +1232 0 obj << /Type /XObject /Subtype /Image @@ -60987,7 +61201,7 @@ endobj /Columns 1280 /Colors 3 >> -/SMask 1225 0 R +/SMask 1233 0 R /Length 81155 >> stream @@ -61252,7 +61466,7 @@ e ::UU{eo޲Fcb t endstream endobj -1225 0 obj +1233 0 obj << /Filter /FlateDecode /Type /XObject @@ -61272,7 +61486,7 @@ stream x1 0!cGz4-d e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@Yd e@W endstream endobj -1226 0 obj +1234 0 obj << /Type /XObject /Subtype /Image @@ -61287,7 +61501,7 @@ endobj /Columns 1172 /Colors 3 >> -/SMask 1227 0 R +/SMask 1235 0 R /Length 17657 >> stream @@ -61353,7 +61567,7 @@ G ` !B!qR endstream endobj -1227 0 obj +1235 0 obj << /Filter /FlateDecode /Type /XObject @@ -61373,7 +61587,7 @@ stream x1 0^ݳ:))bJ@))bJ@))bJ@))bJ@))bJ@))bJ@))bJ@))bJ@))bJ@))bJ@))bJ@))bJ@))bJ@))bJ@))bJ@))bJ@))bJ@))bJ@))bJ@))bJ@))bJ@))bJ@))bJ@))bJ@))bJ@))bJ@ endstream endobj -1228 0 obj +1236 0 obj << /Type /XObject /Subtype /Image @@ -61388,7 +61602,7 @@ endobj /Columns 1848 /Colors 3 >> -/SMask 1229 0 R +/SMask 1237 0 R /Length 539454 >> stream @@ -63099,7 +63313,7 @@ A Ra+FsGQ;ZDdH(d,!p}|Dz5(0+I!=P,bЊ(aCڔl,@L99O#aXN\',;̙u(s;haD"ip8:2pY/7mלv P&Vdwbd /p.C 9<6rhWȤXy8ԸquW c"uͽg$Jcm Qr0{GJ_@DRHҥpp?J)Sݐ1/gl$T]KT0 1桋!ehH$92DK) Zg5 p "ȑy DD$|9#H3 R *s@$Xkp.9 m9m4RƆ#^fJtg2;KLH `Od|vVWD;̈dke)th &NMOo vDD.Z c]olᛴY١̱yo2u:_[3i&C$2~\ yG4 endstream endobj -1229 0 obj +1237 0 obj << /Filter /FlateDecode /Type /XObject @@ -63119,7 +63333,7 @@ stream x1 0g.B*طpk' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@p7 endstream endobj -1230 0 obj +1238 0 obj << /Type /XObject /Subtype /Image @@ -63134,7 +63348,7 @@ endobj /Columns 1840 /Colors 3 >> -/SMask 1231 0 R +/SMask 1239 0 R /Length 515273 >> stream @@ -64821,7 +65035,7 @@ t ADidV)אRys Fǹ J,T'mU;uD5JkBɔ rj+תmMDd7+0`)66m:m?hG^ƫhO~Ui}]f~^s^6$ܩ׵͹MYgꜭZnž0/ :C9t1 endstream endobj -1231 0 obj +1239 0 obj << /Filter /FlateDecode /Type /XObject @@ -64841,7 +65055,7 @@ stream x1 0g>B*طp^ 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L P endstream endobj -1232 0 obj +1240 0 obj << /Type /XObject /Subtype /Image @@ -64856,7 +65070,7 @@ endobj /Columns 1834 /Colors 3 >> -/SMask 1233 0 R +/SMask 1241 0 R /Length 532957 >> stream @@ -66573,7 +66787,7 @@ Cʈ$ ~sHޢ ,/x݀.9ܼKW)U%7®2D y^|*\/> -/SMask 1235 0 R +/SMask 1243 0 R /Length 539494 >> stream @@ -68382,7 +68596,7 @@ r #I#F9Ǎk6<]E圦;pSѺ uh}r @2 h `d RG"tmb3FDtIM Qr0k?jAI!@"ѫKU5(LBƸC1"3ee0ĸI!eD$<"CΥAGsnA$92Bt!dL.7^X2 $9cč'k4r%rn@h97V)a|b;3k{͇MƚRs2rZM-+ۦm7ʍׯΈe7S@i/RM0Zmt>LGNFkN~MIc]j~d6s]Y ׶7]HꜮ5OaGAGXt2 ZldA4sEdqR3L"ZO5I$@"4gL!BI $.BF Й*| \!C7NI'WJArV)"ł$ 3 n5`pRJÌH5muL56ɕB$n pt5S""MY%k4$.-)+[.4rkM)K$~ԏru)nX ).PHWMn\ca=;c s>w隁smF;pl&rnH,7>֥VGFϖ}WuX8Rڤ٣cMʄAde6 2 "uxQ  PLHRb+u=:$^望zӫAG,)tF<….k-k_rRζr7rqS`悈R}luNrorS.@O;dRzG\;My:9eM%+'#nK䶢-o´6$[ȵ{hӏM75P &2 2W* 2 2Td3$A埴"Z*Ƥ;æ霣%rf!ݠ0LcNr\bt.7U&mE*7]?jkR܎õmԣ-:-9ոP.0$"i3t4[n(ޅ!yuu&~2 27{4h endstream endobj -1235 0 obj +1243 0 obj << /Filter /FlateDecode /Type /XObject @@ -68402,7 +68616,7 @@ stream xA 0~U, 8 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4 g@x&4^ endstream endobj -1236 0 obj +1244 0 obj << /Type /XObject /Subtype /Image @@ -68417,7 +68631,7 @@ endobj /Columns 1845 /Colors 3 >> -/SMask 1237 0 R +/SMask 1245 0 R /Length 539274 >> stream @@ -70188,7 +70402,7 @@ B Z~Jȵ9FPiڳ.127!s8ٖk;ntWvr 9.V$ rc:Z7|컪g>=`iLιNۤLdAg3 2Ƞ.0hj@ )YR*R!".gwͯC7\NI` HvY\r+M );k "ulr[or-ɝ]p)z#RKyBpӼIpKrӵ@d bsGc0mIr#xӵvqfVdA;}2 2 +!'(f!IH1+$ĄҊ\Nk59GG:̚C.&D a~Ȝȵn/bt.תswM׏Zn#pm-z#\!#:[MmI"6CG嶻]r7]\m, 2 dAdAdAdAdAXثd{ֲKz݃L-k2uȔm֢`Ӷ$!Q MЊtrvDmF>auCspmyھ$%krn\UbvC~۝/H9w̍SYn;p;}Xs~䜛!$7UV=Mo2EadNL| gXdӂ9L9%Yi`(/O<5*ZtrUnhw4gʍrmVzGuv%"+VcSK;[榃`m~m;,gr׶5bsr2Y[Iw(mÖktڢs;r#:µU9Զ">萛nOu֌8Z6q~`f=Ad/l endstream endobj -1237 0 obj +1245 0 obj << /Filter /FlateDecode /Type /XObject @@ -70208,7 +70422,7 @@ stream xA 0~U, 9 k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T k@P&T ˱ endstream endobj -1238 0 obj +1246 0 obj << /Type /XObject /Subtype /Image @@ -70223,7 +70437,7 @@ endobj /Columns 513 /Colors 3 >> -/SMask 1239 0 R +/SMask 1247 0 R /Length 19372 >> stream @@ -70288,7 +70502,7 @@ OEL %斒ɹXуƗ*t]36F$x\%1%r`|xA|MN,.W[rҥ7AЮ~ 6A9ߌCAO~U(7~7R o??9 _*A7O]U $w+m i+xR& AB$  .hAN x |{ rZ A? B rv@mG '6Ap׵"hA<|ۍJ8JgCA<gJ2g?;oI"/ /hAڗf endstream endobj -1239 0 obj +1247 0 obj << /Filter /FlateDecode /Type /XObject @@ -70308,7 +70522,7 @@ stream x1 0!cG=zX:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P:9us@P瀺5 endstream endobj -1240 0 obj +1248 0 obj << /Type /XObject /Subtype /Image @@ -70323,7 +70537,7 @@ endobj /Columns 1840 /Colors 3 >> -/SMask 1241 0 R +/SMask 1249 0 R /Length 27536 >> stream @@ -70412,7 +70626,7 @@ $־t Jk`o4z\7i'|Z "@'{oOstuZIRa:05m7"( y~  ,QDEa_)aHBcjWjI D@NYĎzegr1scuvr/hBLdIMLz>PJn"]_b/@KӺTz97'j~hBD?j3snE|ڬMُzSB!봞K)O2{AUQ;o槾DIS7OYn>?EQDEb!d1}"("/jB R@j$t:Y Bowgeoxzֱt1;}tF>Z?^ּKu M 9 o_AOhujv,|S tS_5] "("6?)` endstream endobj -1241 0 obj +1249 0 obj << /Filter /FlateDecode /Type /XObject @@ -70432,7 +70646,7 @@ stream x1 0g>B*طp^ 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L 0 0L ,<E endstream endobj -1242 0 obj +1250 0 obj << /Type /XObject /Subtype /Image @@ -70447,7 +70661,7 @@ endobj /Columns 171 /Colors 3 >> -/SMask 1243 0 R +/SMask 1251 0 R /Length 1285 >> stream @@ -70456,7 +70670,7 @@ D Ǥ[V(}$BaDz}7nWTrK}g{,xyŒ2xOv8⸸BD)"vvt455QRkQ+zڴ绻m=}"}Qt@erZeeU]]-9YyoY =q%3^ʘ%<̙!c` @~3ӳg3ufcbB˹-v룄kX' !"IEmEVe0|ٖJ @(s>זʫ$k@@6|@n|foEEٔ$۶SJEI޸yˮ;&N|vҤ  DF]F; A(DQ=Q8^iRJ("RJ 93ZM"ˠ1rq.נ(ᄎj5”ɓ-iiܺ$>A<0EEQ׀Ds6/uO8PFED%@#ceO7.Շ4V:F)1rt @ nCc<ּM4pQc ѽ)|"2?Q?Du@: QuT: endstream endobj -1243 0 obj +1251 0 obj << /Filter /FlateDecode /Type /XObject @@ -70476,7 +70690,7 @@ stream xA 1g`F]22222222e# endstream endobj -1244 0 obj +1252 0 obj << /Type /XObject /Subtype /Image @@ -70491,7 +70705,7 @@ endobj /Columns 171 /Colors 3 >> -/SMask 1245 0 R +/SMask 1253 0 R /Length 1135 >> stream @@ -70500,7 +70714,7 @@ x 3+]!Q)^*̻ &[xPSV2IZF@9Or_@TzdyZ.7PNp9ͣ0X> -/SMask 1247 0 R +/SMask 1255 0 R /Length 257079 >> stream @@ -71347,7 +71561,7 @@ D ZU62l98]/xRvVf;\s 7M}{Ω*tVIYMV7,Q} llQϞZq2ڨv+ەt*>qkxz3l8vInuƌōֳkNyVI&7p}sx$#js<(` @YYTo~8F< qMVǬD}Y乙Җ f~~CpQjfϚR9VWEcrڿtsD}fgu0o)}gW9Gٜκmвfՙn)%IL~j2M[`V"rVfE.oY{noXgmFܯ^ԇr\g=&&֤ R)Ik)XK)Yɋۤ+JY\.YOOzgJ^Jԝ<$Lf\\4aIu;vT28٤(6Y"۩Q/- YmK>oEe`F^cCb,)b EڒWR:3 Nv6 ;uL΁u_,}er趿ώ(K N y7^ZoYL#L_6#JhmymIR>Gǎ~n~ZKnrǟRw_}gBպ]}.H:Nfgeɉ{[ ̾l.^1òذVO ҕzm|ZTu%[ok'm3^3aiGG'sK)YN Nl7]g^ǰZ2)CQtp5Smu]nNc9wu嗽kKrW}Z/ș#TGН]=qιy^,\.3[uotuņd>|MAooonpN7|&#9OOOG7o> -/SMask 1249 0 R +/SMask 1257 0 R /Length 21101 >> stream @@ -71452,7 +71666,7 @@ W+8J ,vD!avD!.B!\r5\c6ϸfIIɖ-[~FmGBI ""km|vD!DDD4}z󨾇0;"B8ޙb- endstream endobj -1249 0 obj +1257 0 obj << /Filter /FlateDecode /Type /XObject @@ -71472,7 +71686,7 @@ stream xA 0CBBZ{lςyE_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_.^F endstream endobj -1250 0 obj +1258 0 obj << /Type /XObject /Subtype /Image @@ -71487,7 +71701,7 @@ endobj /Columns 1848 /Colors 3 >> -/SMask 1251 0 R +/SMask 1259 0 R /Length 506775 >> stream @@ -73048,7 +73262,7 @@ A a$ FPDkڠܰt[nx}Fͪb FDجo"O]%Wf[n͐D㨽&*1r;ײZro m~rr Fg$Ir[F!M>i,$H"= ɴI$DI=KDI$DI$DI$D'ft{m/`It0\$-2$~H6xS0cXjb̴&DrDԶFeEl^aɵM}p-l}-o%&-ljb\}Pk [c ƹJmnOk6m;s]ݲS٪ݒ$,I$# vɮDI``)9 ʨpͯZH.]ob`s#k\Kn<5\:Q:V]܈9:P%8Y5\Ke:˘Am)B6XggMn,͟-1O%DI endstream endobj -1251 0 obj +1259 0 obj << /Filter /FlateDecode /Type /XObject @@ -73068,7 +73282,7 @@ stream x1 0g.B*طpk' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`' q@`m endstream endobj -1252 0 obj +1260 0 obj << /Type /XObject /Subtype /Image @@ -73083,7 +73297,7 @@ endobj /Columns 1821 /Colors 3 >> -/SMask 1253 0 R +/SMask 1261 0 R /Length 539259 >> stream @@ -74974,7 +75188,7 @@ n` MdO"WPJww*\ qt[)b)1;<7-- m5 bI7E3W; ; -aHPsݻoݯ{緐 endstream endobj -1253 0 obj +1261 0 obj << /Filter /FlateDecode /Type /XObject @@ -74994,7 +75208,7 @@ stream xձ03>\Эs^w@w endstream endobj -1254 0 obj +1262 0 obj << /Type /XObject /Subtype /Image @@ -75009,7 +75223,7 @@ endobj /Columns 1845 /Colors 3 >> -/SMask 1255 0 R +/SMask 1263 0 R /Length 544180 >> stream @@ -76828,7 +77042,7 @@ z¾ nٸ=Onۆe۸ZEPTT*xœ@Ӵ.L> -/SMask 1257 0 R +/SMask 1265 0 R /Length 30419 >> stream @@ -76966,7 +77180,7 @@ z0\ dϦY.+1Z 2ddk\,% @)˛Wj%YjY9vغEHcW7U 6waQzZ/t)))ZzZ*eyJRRk/T,_\fXU:Yye-/""**.Դtдy%QQq(2SYn+2EHNR7"tZ<tzf[fki8) V_:;7==wv~BvZ7E۽8A:бfe3R*7Rٖrʘm<-[-J٭N-RVVfefh/yMqYzh5%4;c>-^N3rrfPplNyS3 *R׬-n[$ǕWFT633؝8(ojN;8$5$?؝]Y433'̩榷1m*S)ګmwTfd|(((\Y޼rw>̌s,2Lw~F2"( ZY_'hYGx[WzniqRYm\vde*xDGKϝKY#sKS# *_Q9 7u^#Ї`d(!,twHS}G3#(uYg]yNs,[?w(uEu$؈())骫w(#V­ZYYβ~%  u0ڙRJ 1#D8yG3 Q@:@T*@T@FV̺ed[h4QGVtv#GHGR|8ԴUBX(hѢܼlٲN, >#w0C:ٿ endstream endobj -1257 0 obj +1265 0 obj << /Filter /FlateDecode /Type /XObject @@ -76986,7 +77200,7 @@ stream xA 0CBBZ{lςyE_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_.u# endstream endobj -1258 0 obj +1266 0 obj << /Type /XObject /Subtype /Image @@ -77001,7 +77215,7 @@ endobj /Columns 582 /Colors 3 >> -/SMask 1259 0 R +/SMask 1267 0 R /Length 24987 >> stream @@ -77077,7 +77291,7 @@ y< xB]??<Nwۂ}PL;hr^W![iC]G7mZ?xG> -/SMask 1261 0 R +/SMask 1269 0 R /Length 14549 >> stream @@ -77171,7 +77385,7 @@ YeG (hȡ P5G Fi0So~QVC姨 03;m$r6H;J7䖫i_z4t endstream endobj -1261 0 obj +1269 0 obj << /Filter /FlateDecode /Type /XObject @@ -77196,7 +77410,7 @@ x AqG endstream endobj -1262 0 obj +1270 0 obj << /Type /XObject /Subtype /Image @@ -77211,7 +77425,7 @@ endobj /Columns 872 /Colors 3 >> -/SMask 1263 0 R +/SMask 1271 0 R /Length 16208 >> stream @@ -77272,7 +77486,7 @@ H!  6ia}5 8T3rlcsyLEN>B gfʲvJҿ?悟3?۲36%v`` KAcwqƚn9aKr4eU`TNO&`,}]kЁ%CvA?!nQCP#_[]".{[:r4[y:kR"KhUJ&(~e7ƈa=ܭB,`,\K|Rl4NwSYgbۜt=+m^Sտ?kW11\$]4 %.z‘$I܌ E8*ꝍV~h̀^S| MalXWTy\AYD"z[}EWW RDR9(*a|jޗ'b OaDT:QL B$p,Uf{&\s`9//v_`RG~B p}" N2",&(*_rCJJ(leFc33thB}tNf2:E9CڈMdӊW{ WҦRdlT "AMSJ"BYv%F§[*bffz47M$<[vګ ADi6˫ruMSҶ5&xl endstream endobj -1263 0 obj +1271 0 obj << /Filter /FlateDecode /Type /XObject @@ -77292,7 +77506,7 @@ stream xӱ03WArA= `4 F `4 F `4 F `4 F `4 F `4 F p5 endstream endobj -1264 0 obj +1272 0 obj << /Type /XObject /Subtype /Image @@ -77307,7 +77521,7 @@ endobj /Columns 607 /Colors 3 >> -/SMask 1265 0 R +/SMask 1273 0 R /Length 18321 >> stream @@ -77387,7 +77601,7 @@ t `tB:X!VHG-78 endstream endobj -1265 0 obj +1273 0 obj << /Filter /FlateDecode /Type /XObject @@ -77407,7 +77621,7 @@ stream xA 0CBBZ{lςyE_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_E_.յ endstream endobj -1266 0 obj +1274 0 obj << /Type /XObject /Subtype /Image @@ -77422,7 +77636,7 @@ endobj /Columns 952 /Colors 3 >> -/SMask 1267 0 R +/SMask 1275 0 R /Length 321098 >> stream @@ -78497,7 +78711,7 @@ T* Xl||<cAA20 i݀^   9   r(PFAAsL1 endstream endobj -1267 0 obj +1275 0 obj << /Filter /FlateDecode /Type /XObject @@ -78517,7 +78731,7 @@ stream xA 0C/U, Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bq!ȸd\2. Aƅ Bmk endstream endobj -1268 0 obj +1276 0 obj << /Type /XObject /Subtype /Image @@ -78532,14 +78746,14 @@ endobj /Columns 97 /Colors 3 >> -/SMask 1269 0 R +/SMask 1277 0 R /Length 223 >> stream x Eu3ae\t> -/SMask 1271 0 R +/SMask 1279 0 R /Length 237 >> stream x Dqe@CasB!2 ӽYfXkk"Ԥxv:Al}/.ȝGȐ$Z `& ,."PYz'FSᬻJpy]0ƕ.Թ*dlpj 2H DJI HkvvMg~zO:M*YґוB!:~ endstream endobj -1271 0 obj +1279 0 obj << /Filter /FlateDecode /Type /XObject @@ -78602,7 +78816,7 @@ stream xA  gMq@oELz^zSG endstream endobj -1272 0 obj +1280 0 obj << /Type /XObject /Subtype /Image @@ -78617,14 +78831,14 @@ endobj /Columns 97 /Colors 3 >> -/SMask 1273 0 R +/SMask 1281 0 R /Length 271 >> stream x De{HXEMξc\D!B!Y\N跟kY7PDiZFuV Qڑ!MbT .ao #ӭ13ysNG'S^P5J (g ;#k Z8_>2چ[ۻ;1G#˧o%w =2>W<`>2~I^M3Q ٻeAuyF}Os۔B!Н4 endstream endobj -1273 0 obj +1281 0 obj << /Filter /FlateDecode /Type /XObject @@ -78645,7 +78859,7 @@ x & `0 ëG endstream endobj -1274 0 obj +1282 0 obj << /Type /XObject /Subtype /Image @@ -78660,7 +78874,7 @@ endobj /Columns 348 /Colors 3 >> -/SMask 1275 0 R +/SMask 1283 0 R /Length 1085 >> stream @@ -78678,7 +78892,7 @@ t? >z)gX6ũ+)rUj.eH*}N4O>B-kΖ~JQ*m!)UV14y endstream endobj -1275 0 obj +1283 0 obj << /Filter /FlateDecode /Type /XObject @@ -78698,7 +78912,7 @@ stream x @7.؞E!sC 27dnܐ!sC 27dnܐ!sC 27dnܐ!sC 27dnܐ!sC 27dnܐ!sC 27dnܐ!sC 27dnܐ!sC 27dnܐ!sC 27dnܐ!sC 27dnܐ!sC 27dnܐ!sC 27dnܐ!sC 27dnܐ!sC 27dnܐ!sC 27dnܐ!sC 27dnܐ!sC 27dnܐ!sC 27t endstream endobj -1276 0 obj +1284 0 obj << /Type /XObject /Subtype /Image @@ -78713,7 +78927,7 @@ endobj /Columns 388 /Colors 3 >> -/SMask 1277 0 R +/SMask 1285 0 R /Length 1115 >> stream @@ -78722,7 +78936,7 @@ x a$SNO7hY.UA[޽/M݈N~Y33JKZGAF_?lŗ$0Z'!{'(9aBg, \&Vŗ6>086JUhxJq7 {iimA2};8QëGt`q_XՔ endstream endobj -1277 0 obj +1285 0 obj << /Filter /FlateDecode /Type /XObject @@ -78743,7 +78957,7 @@ x z;mo;L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L0!L endstream endobj -1278 0 obj +1286 0 obj << /Type /XObject /Subtype /Image @@ -78758,7 +78972,7 @@ endobj /Columns 101 /Colors 3 >> -/SMask 1279 0 R +/SMask 1287 0 R /Length 2562 >> stream @@ -78769,7 +78983,7 @@ x '2 %Λ`Q(Fn":g{Y]:-^hhx9c'Y4ap2}vSc=}B8G+':%"n.>ω.ECf[D^/T4FZnyg.8 V0~r4D9h|C?_ϗ9e|} endstream endobj -1279 0 obj +1287 0 obj << /Filter /FlateDecode /Type /XObject @@ -78790,7 +79004,7 @@ x &X,bX,a9\QG endstream endobj -1280 0 obj +1288 0 obj << /Type /XObject /Subtype /Image @@ -78805,7 +79019,7 @@ endobj /Columns 101 /Colors 3 >> -/SMask 1281 0 R +/SMask 1289 0 R /Length 2208 >> stream @@ -78823,7 +79037,7 @@ B bmmkD"QrrR$IXa5t($hv:( GB~_oL8"a/xm6#G&oָGT@m$~ݗh>og&3pAu#{gtٟX7W^D~_B|C/~i endstream endobj -1281 0 obj +1289 0 obj << /Filter /FlateDecode /Type /XObject @@ -78844,7 +79058,7 @@ x &X,bX,a9\QG endstream endobj -1282 0 obj +1290 0 obj << /Type /XObject /Subtype /Image @@ -78859,7 +79073,7 @@ endobj /Columns 101 /Colors 3 >> -/SMask 1283 0 R +/SMask 1291 0 R /Length 292 >> stream @@ -78867,7 +79081,7 @@ x :Px9 }/O_Tsُ$Q͋_xҗ9\/gm~͘yO)a?E3/*Ym ^*slޭ_Tn^nfYm\w~LY=)Kd[JVEf>~ul!_fGڌ Gj3܍z__ /|1!_ bCŐ/|1>lQ endstream endobj -1283 0 obj +1291 0 obj << /Filter /FlateDecode /Type /XObject @@ -78888,7 +79102,7 @@ x &X,bX,a9\QG endstream endobj -1284 0 obj +1292 0 obj << /Type /XObject /Subtype /Image @@ -78903,7 +79117,7 @@ endobj /Columns 404 /Colors 3 >> -/SMask 1285 0 R +/SMask 1293 0 R /Length 9205 >> stream @@ -78932,7 +79146,7 @@ mnn"_ 藠 A 4JThEO,LT< <b> endstream endobj -1285 0 obj +1293 0 obj << /Filter /FlateDecode /Type /XObject @@ -78967,7 +79181,7 @@ x Qd (@Fi(8RVa9"OWV7W٬ &rRR%Ѷq)'@DJۗ\Q "}ܜfH_D endstream endobj -1286 0 obj +1294 0 obj << /Type /XObject /Subtype /Image @@ -78982,7 +79196,7 @@ endobj /Columns 404 /Colors 3 >> -/SMask 1287 0 R +/SMask 1295 0 R /Length 1346 >> stream @@ -78993,7 +79207,7 @@ i JyEj?Wۮ:3/}~OUuzGx΄﷥@N묓:wxvwߜY펹F?W=*)$H $H $H $H $H $H $H $H $H $H $H $H $H $H $H $H $H $H $H $H $?kx endstream endobj -1287 0 obj +1295 0 obj << /Filter /FlateDecode /Type /XObject @@ -79013,7 +79227,7 @@ stream x 0_o +T3f>˚N7eJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)AdJ)A. endstream endobj -1288 0 obj +1296 0 obj << /Type /XObject /Subtype /Image @@ -79028,7 +79242,7 @@ endobj /Columns 202 /Colors 3 >> -/SMask 1289 0 R +/SMask 1297 0 R /Length 687 >> stream @@ -79036,7 +79250,7 @@ x ўc[c3H+jpk=oKO&C@}hԑJipMM%zxr*[}m=&Zn}bg@@'.Os4S-{.F>oT0in{&싣|4% % % % % % % % % % % % % % % % % % % % % % ?V endstream endobj -1289 0 obj +1297 0 obj << /Filter /FlateDecode /Type /XObject @@ -79056,7 +79270,7 @@ stream x 0g-!8K=~^ӊQ+VDZ"jEԊQ+VDZ"jEԊQ+VDZ"jEԊQ+VDZ"jEԊQ+* endstream endobj -1290 0 obj +1298 0 obj << /Type /XObject /Subtype /Image @@ -79071,7 +79285,7 @@ endobj /Columns 75 /Colors 3 >> -/SMask 1291 0 R +/SMask 1299 0 R /Length 1771 >> stream @@ -79083,7 +79297,7 @@ x  )x?⽛ÈՓcP @+&QʅJn$tesSTaBޓ|uR(xGޓ3PY :R7h(vG)GCbΨ*Fj(w{'41}Ox;@l=yXQs$zk,D7|ιE#\((솏~coH0)))5Tna'WP!qH`홅ɛG^.}d_Uv=%IޮgvO;Jo4ã] }]hWLW*8@GzvxԙJ6Y<¡Rg5Y9=9#A(ЪA\ʟ@0j[鴕(K +neē|YxXɭB2f&qewO endstream endobj -1291 0 obj +1299 0 obj << /Filter /FlateDecode /Type /XObject @@ -79103,7 +79317,7 @@ stream xA0>!:wr\.U0uG endstream endobj -1292 0 obj +1300 0 obj << /Type /XObject /Subtype /Image @@ -79118,7 +79332,7 @@ endobj /Columns 75 /Colors 3 >> -/SMask 1293 0 R +/SMask 1301 0 R /Length 1753 >> stream @@ -79133,7 +79347,7 @@ E D7 T(#wkd朤B5֚!6~Ko8n endstream endobj -1293 0 obj +1301 0 obj << /Filter /FlateDecode /Type /XObject @@ -79153,7 +79367,7 @@ stream xA0>!:wr\.U0uG endstream endobj -1294 0 obj +1302 0 obj << /Type /XObject /Subtype /Image @@ -79168,7 +79382,7 @@ endobj /Columns 1347 /Colors 3 >> -/SMask 1295 0 R +/SMask 1303 0 R /Length 9550 >> stream @@ -79182,7 +79396,7 @@ x >kw@'J:QЉN<t%(yD@'J:QЉN<t%(yD@'J:QЉN<t%(yd:I֖RSU5n6> -/SMask 1297 0 R +/SMask 1305 0 R /Length 40741 >> stream @@ -79615,7 +79829,7 @@ a ne!,    nС  V%t(  U   `UB2  XС  V%t(  U   `U?1Q endstream endobj -1297 0 obj +1305 0 obj << /Filter /FlateDecode /Type /XObject @@ -79636,7 +79850,7 @@ x t*up2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap2 !Ap5 endstream endobj -1298 0 obj +1306 0 obj << /Type /XObject /Subtype /Image @@ -79651,14 +79865,14 @@ endobj /Columns 420 /Colors 3 >> -/SMask 1299 0 R +/SMask 1307 0 R /Length 1429 >> stream xA A[r&PM#SlBW)Wgw~AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; As<; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; Azt v@ b$ v@ b$ v@ b$ v@ b$ v@ b$ v@ b$ v@ b$ v@ b$ v@ b$ v@ b$ v@ b$ v@ b$ v@ b$ v@ b$ v@ b$ v@ b$ v@ b$ v@ b$ v@ b$ v@ b$ v@ c_WoγH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; a\nH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH; AH8 endstream endobj -1299 0 obj +1307 0 obj << /Filter /FlateDecode /Type /XObject @@ -79783,7 +79997,7 @@ x 8M1G endstream endobj -1300 0 obj +1308 0 obj << /Type /XObject /Subtype /Image @@ -79866,7 +80080,7 @@ K (>|Cqx_QKZ%.J`̮$++O~!Fl$ Nۢ 0d, 8M}W@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@W=~5 ܋?7H lN$+9T݌y@ endstream endobj -1301 0 obj +1309 0 obj << /Type /XObject /Subtype /Image @@ -79881,7 +80095,7 @@ endobj /Columns 22 /Colors 3 >> -/SMask 1302 0 R +/SMask 1310 0 R /Length 877 >> stream @@ -79893,7 +80107,7 @@ x endstream endobj -1302 0 obj +1310 0 obj << /Filter /FlateDecode /Type /XObject @@ -79913,7 +80127,7 @@ stream xcπ 0a0,# endstream endobj -1303 0 obj +1311 0 obj << /Type /XObject /Subtype /Image @@ -79928,7 +80142,7 @@ endobj /Columns 22 /Colors 3 >> -/SMask 1304 0 R +/SMask 1312 0 R /Length 825 >> stream @@ -79937,7 +80151,7 @@ x U6}󯯗R}L_f#ݬNrS-l ZǃΙov;p\e-WO/MW#4T '3wVSV%'Xܪ(0$ĀNb%7kk:Vd 9m j9¸{LoN endstream endobj -1304 0 obj +1312 0 obj << /Filter /FlateDecode /Type /XObject @@ -79957,7 +80171,7 @@ stream xcπ 0a0,# endstream endobj -1305 0 obj +1313 0 obj << /Type /XObject /Subtype /Image @@ -79972,14 +80186,14 @@ endobj /Columns 216 /Colors 3 >> -/SMask 1306 0 R +/SMask 1314 0 R /Length 875 >> stream xNH@QgهH ZvFgP\,Gp_?}pB$B$! DHI" B$A$! DHI" B$A$! DHI" B$A$! DHI" B$A$! DHI" B$A$󜏹nm1yϻ-=\^t{o)֪ϟZi^.2mmޒiyko~h\>opk=)įB6$^M=}[4CtCdB|M!zu3!2I" B$A$! D!d׹HB$A$! DHI" B$A$! DHI" B$A$! DHI" B$!kz׹H[N\m"K$! DHV!wt.s9!~oo6uR43罼fu91$~olECgOᬵ!f/ESOB oU9۴y*y/}Xa%!/7WyW# DHI" B$aIJWyW#*pNr w+mڼ#>cڼ=)D;+$! DHI" B$A$! DHI" B$A$! DHI" B$A$! DHI" B$A$! DHI" B$A$! DHI" B$A$I endstream endobj -1306 0 obj +1314 0 obj << /Filter /FlateDecode /Type /XObject @@ -79999,7 +80213,7 @@ stream xρ 03WT ǟ[4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLs o endstream endobj -1307 0 obj +1315 0 obj << /Type /XObject /Subtype /Image @@ -80014,7 +80228,7 @@ endobj /Columns 216 /Colors 3 >> -/SMask 1308 0 R +/SMask 1316 0 R /Length 1288 >> stream @@ -80041,7 +80255,7 @@ $ `B0D! Q΁\ endstream endobj -1308 0 obj +1316 0 obj << /Filter /FlateDecode /Type /XObject @@ -80061,7 +80275,7 @@ stream xρ 03WT ǟ[4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLs o endstream endobj -1309 0 obj +1317 0 obj << /Type /XObject /Subtype /Image @@ -80076,7 +80290,7 @@ endobj /Columns 216 /Colors 3 >> -/SMask 1310 0 R +/SMask 1318 0 R /Length 1479 >> stream @@ -80088,7 +80302,7 @@ Qa Bs_5kOH DH DH DHX3bDBlD!!!aUOGzߑ'[ߋb3X$ŊDW^sqg7mn@~LM9y8#/Nxh6 %"$XX꺮 "$"$"$"$& endstream endobj -1310 0 obj +1318 0 obj << /Filter /FlateDecode /Type /XObject @@ -80108,7 +80322,7 @@ stream xρ 03WT ǟ[4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLӘ1Mc4i4iLs o endstream endobj -1311 0 obj +1319 0 obj << /Type /XObject /Subtype /Image @@ -80123,7 +80337,7 @@ endobj /Columns 4961 /Colors 3 >> -/SMask 1312 0 R +/SMask 1320 0 R /Length 3565035 >> stream @@ -93067,7 +93281,7 @@ SW 0=QшLF`z4ӣ(V endstream endobj -1312 0 obj +1320 0 obj << /Filter /FlateDecode /Type /XObject @@ -93087,7 +93301,7 @@ stream x1 0gB*ؽpm9lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lpرcAփ[ aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lĎ ða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  vX` Fp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða؀ڱcAփ[aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68 7 endstream endobj -1313 0 obj +1321 0 obj << /Type /XObject /Subtype /Image @@ -93102,7 +93316,7 @@ endobj /Columns 4961 /Colors 3 >> -/SMask 1314 0 R +/SMask 1322 0 R /Length 3761458 >> stream @@ -107304,7 +107518,7 @@ lCF` 0x/v*#} endstream endobj -1314 0 obj +1322 0 obj << /Filter /FlateDecode /Type /XObject @@ -107324,7 +107538,7 @@ stream x1 0gB*ؽpm9lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lpرcAփ[ aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lĎ ða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  vX` Fp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða؀ڱcAփ[aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68 7 endstream endobj -1315 0 obj +1323 0 obj << /Type /XObject /Subtype /Image @@ -107339,7 +107553,7 @@ endobj /Columns 1920 /Colors 3 >> -/SMask 1316 0 R +/SMask 1324 0 R /Length 483438 >> stream @@ -108889,7 +109103,7 @@ C ΠiuR=!H!$4 )IA He-& endstream endobj -1316 0 obj +1324 0 obj << /Filter /FlateDecode /Type /XObject @@ -108910,7 +109124,7 @@ x u^;uȀ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`\T i endstream endobj -1317 0 obj +1325 0 obj << /Type /XObject /Subtype /Image @@ -108925,14 +109139,14 @@ endobj /Columns 108 /Colors 3 >> -/SMask 1318 0 R +/SMask 1326 0 R /Length 484 >> stream xю /{oi"̲3oMa,в,ˏ݁;D%(@JP"D%(@JP"ss)e3tYcZI{$~jk\J{n%NgXbzP.q+Ex$f5ewˉ)$K >$γ>˲ԇƫA~DL~/ۺ{Ҙ{yAt*hE'_N{Ě+LD%(@JP"D%(@JP"D%(@JP"D%U endstream endobj -1318 0 obj +1326 0 obj << /Filter /FlateDecode /Type /XObject @@ -108953,7 +109167,7 @@ x [9d2L&d2L&d2L&d2L&d2L&{A endstream endobj -1319 0 obj +1327 0 obj << /Type /XObject /Subtype /Image @@ -108968,7 +109182,7 @@ endobj /Columns 108 /Colors 3 >> -/SMask 1320 0 R +/SMask 1328 0 R /Length 451 >> stream @@ -108976,7 +109190,7 @@ x 7ⷒDj?1&ލVmJNTfbA G3hQ3Q^uDE@9\'>bsKX޵p:Bc2q:'wf6r8q#Oĉzo[wr63ˆ8]hi}zt?G*,ϟa_w}KCguULڙ3+tn0"?.scu X.gF`DF`DF`DF`DF`DF`DFu endstream endobj -1320 0 obj +1328 0 obj << /Filter /FlateDecode /Type /XObject @@ -108997,7 +109211,7 @@ x [9d2L&d2L&d2L&d2L&d2L&{A endstream endobj -1321 0 obj +1329 0 obj << /Type /XObject /Subtype /Image @@ -109012,14 +109226,14 @@ endobj /Columns 108 /Colors 3 >> -/SMask 1322 0 R +/SMask 1330 0 R /Length 422 >> stream xn0Ѹs\?ʜe Sj>3{F`DF`DF`DF`DF`DoR.w'KtNCo =s#z)qMеC۝A'v {u$RSqshGp"L?jx,q׈J7#0"#0"#0"#0"#0"#0"n?Q>c5fұ;G- ;}wi暸X=I ?D1:DIy9Gg1p_蝿ZB;yw`DF`DF`DF`DF`DF`DF endstream endobj -1322 0 obj +1330 0 obj << /Filter /FlateDecode /Type /XObject @@ -109040,7 +109254,7 @@ x [9d2L&d2L&d2L&d2L&d2L&{A endstream endobj -1323 0 obj +1331 0 obj << /Type /XObject /Subtype /Image @@ -109055,14 +109269,14 @@ endobj /Columns 108 /Colors 3 >> -/SMask 1324 0 R +/SMask 1332 0 R /Length 397 >> stream xN@@Q1/hڌp!=vns>W+("PD@"E("PD@"E("PD@"_g޼mO] +q^y~5נIs`JmpFbc^]q<뱽1lg0q0VȌmڪ}(Eogn|ݾjE3ef{|Z>v("PD@"E("PD@"E("PD@"EIQ endstream endobj -1324 0 obj +1332 0 obj << /Filter /FlateDecode /Type /XObject @@ -109083,7 +109297,7 @@ x [9d2L&d2L&d2L&d2L&d2L&{A endstream endobj -1325 0 obj +1333 0 obj << /Type /XObject /Subtype /Image @@ -109098,7 +109312,7 @@ endobj /Columns 1768 /Colors 3 >> -/SMask 1326 0 R +/SMask 1334 0 R /Length 68446 >> stream @@ -109257,7 +109471,7 @@ B {Oʒ 'ݩrkW!H tZy=-{ZZ4z\C7ұgoki' }ַ3mU%|*++Nw9Ǽpypj%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9%9ƙL\d2PWWهx<Y(HL&y2?????7xܜ= ql6N8'dQQQeee{{p \|!>JJJ82̱l6b{\MWWWgggnnnCCC{{{___IIIMMMKKp \L&JOORL&ͦ|J endstream endobj -1326 0 obj +1334 0 obj << /Filter /FlateDecode /Type /XObject @@ -109277,7 +109491,7 @@ stream xA 0C!YZm;^2:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ4 H3:Ҍ QB endstream endobj -1327 0 obj +1335 0 obj << /Type /XObject /Subtype /Image @@ -109292,7 +109506,7 @@ endobj /Columns 755 /Colors 3 >> -/SMask 1328 0 R +/SMask 1336 0 R /Length 7230 >> stream @@ -109307,7 +109521,7 @@ $ >.4FogU[}(el> ЇP6@C}(el> ЇP6@C}(el>f>Sx;.~ޭj+x[2xy*YOFsC6f1;wP[s8ƫ=fl> ЇP6@C}(el> ЇP6@C}(el> ЇP6@u^Oz5zO`\%a2$q7'o_u.ۏcv!? чbX)M]n1kE}(el> ЇP6@C}(el> ЇP6@C}(e1[ euWxpd 8^ϗz7Ϝh>@lN ЇP6@o endstream endobj -1328 0 obj +1336 0 obj << /Filter /FlateDecode /Type /XObject @@ -109328,7 +109542,7 @@ x ztς;<5xSyj> -/SMask 1330 0 R +/SMask 1338 0 R /Length 6746 >> stream @@ -109428,7 +109642,7 @@ m <7 6 6 6 I endstream endobj -1330 0 obj +1338 0 obj << /Filter /FlateDecode /Type /XObject @@ -109558,7 +109772,7 @@ x N'BQ endstream endobj -1331 0 obj +1339 0 obj << /Type /XObject /Subtype /Image @@ -109573,7 +109787,7 @@ endobj /Columns 1920 /Colors 3 >> -/SMask 1332 0 R +/SMask 1340 0 R /Length 295467 >> stream @@ -110551,7 +110765,7 @@ I БH$D"H$D"H$9"D:D"H$D"H$D"G(@G"H$D"H$D"HH$D"H$D"H$N޳w endstream endobj -1332 0 obj +1340 0 obj << /Filter /FlateDecode /Type /XObject @@ -110572,7 +110786,7 @@ x u^;uȀ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ p! e endstream endobj -1333 0 obj +1341 0 obj << /Type /XObject /Subtype /Image @@ -110587,7 +110801,7 @@ endobj /Columns 1920 /Colors 3 >> -/SMask 1334 0 R +/SMask 1342 0 R /Length 285963 >> stream @@ -111624,7 +111838,7 @@ jP Њr1EQEQEQEQEQAhEQEQEQEQEQeAPZQEQEQEQEQEYTVEQEQEQEQEQEQEQEQEQEQAhEQEQEQEQEQeAPZQEQEQEQEQEYTVEQEQEQEQEQEQEQEQEQEQAhEQEQEQEQEQeAx endstream endobj -1334 0 obj +1342 0 obj << /Filter /FlateDecode /Type /XObject @@ -111645,7 +111859,7 @@ x u^;uȀ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ p! e endstream endobj -1335 0 obj +1343 0 obj << /Type /XObject /Subtype /Image @@ -111660,7 +111874,7 @@ endobj /Columns 1920 /Colors 3 >> -/SMask 1336 0 R +/SMask 1344 0 R /Length 1112571 >> stream @@ -115708,7 +115922,7 @@ S} Ԡ^AA)ǑĠ)'A)L*@+r*GGG~z`9jvܽbŲ˗ɟ?~![~Ŋeiy̛7d`s / Mӿ?}ptt߼_:swo~du饗\pJ ?gdGze9H|e'0S%A"PԠIkPZW"'A)L*@+rR>MӇ~dk]ΝbEs˖ƷB7>7ҥg<3f oڴoki>_~_Zw~Qbnڵ{Gv>joYMT<NATjP'9jPr\8 J+d6(EQiBhEQNMjM7c|lc=ٵkWOy4ڵ3H 7p}۵kAF>ZrWcTo%76M͛+9吨ArP5(8LQEQ^.A*I endstream endobj -1336 0 obj +1344 0 obj << /Filter /FlateDecode /Type /XObject @@ -115729,7 +115943,7 @@ x u^;uȀ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ `0`0  @ p endstream endobj -1337 0 obj +1345 0 obj << /Type /XObject /Subtype /Image @@ -115744,7 +115958,7 @@ endobj /Columns 1366 /Colors 3 >> -/SMask 1338 0 R +/SMask 1346 0 R /Length 1564922 >> stream @@ -121242,7 +121456,7 @@ TT V(&I endstream endobj -1338 0 obj +1346 0 obj << /Filter /FlateDecode /Type /XObject @@ -121359,7 +121573,7 @@ x U.Z endstream endobj -1339 0 obj +1347 0 obj << /Type /XObject /Subtype /Image @@ -121374,7 +121588,7 @@ endobj /Columns 1847 /Colors 3 >> -/SMask 1340 0 R +/SMask 1348 0 R /Length 542509 >> stream @@ -123201,7 +123415,7 @@ f LKL$ fpI9I,9M7< FL%@3(}^x‘Ñ PpihȣX IB2~$HZx2&3vf<&Eܕ G~h&fmNfh$b[ɘRyZ<Շjrx`SPVtf+טͤϕȍЦ7  Ɠ6  0fDBEOV/t\d/M WQ8d⿓+uF咤f&yN3LY\l3 eђ؈$KPڨXXHHVɫ͒55LѨ(,;S"$)kY|dHE)m"K+Z)$$5\rKfR*f\8@LxR)~ΎtC!Hj&R,?]DHs45fܚ+ex/ njj߸ef*th(T{9١ D!ʆ,FYJD7|cHVe{̈BoblF2QI;\-*^dj(9i250$4ƄL[= }dC23[vLA% CNW,܋' V4G|1Y0`'IS1``cLv P3 ~½kښA3nLPEEbz1@`,n%,35&%Ƈ(Dda!Q,%+~1aCp9`^=eaBH1lZ 3F=1 dqȤT!3ʋlDXT,DWK7Mjz50,Rs8&3&>5I 0H~/@44b;]Ύx0(]"Θ:>D΁φjXK:4r5*VAwwΘuN9P?-+}r/g1m &Gfh!{OiQL u!LJI[v9c2x&-j.Mkdl E|Zl>waL@}IBģ _;G:qj,+BAAAALy`0XZZJ*>      AAAAA== AAAAA<ua+++B K.V7W%    xh4ZP(~aaҥKw `uuƍoo}Z>ik     58A9^[VV3ٟPKAAAAq,,,lr.t /tAAAAAģ7wsvc9SSSkkkO~^[ZZzҦAAAAA<#t:ͭz1D*-0榧M ?&eoW.?m?nm?XWx6 Rٛ #S*i?Z\\AA<:h!O \]I]޷_oU_/׾& |a|bWNf˿4ARb7?<0/??-B)׾^}vx~ɇ 1W*Gkkk>" Cr39_67O^y}+iӆx}핅aت?sB./֭nnnm ] 7/οo[J=:'̅հ endstream endobj -1340 0 obj +1348 0 obj << /Filter /FlateDecode /Type /XObject @@ -123221,7 +123435,7 @@ stream xA 0~U, :# o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&t o@&tGk endstream endobj -1341 0 obj +1349 0 obj << /Type /XObject /Subtype /Image @@ -123236,7 +123450,7 @@ endobj /Columns 4961 /Colors 3 >> -/SMask 1342 0 R +/SMask 1350 0 R /Length 3320669 >> stream @@ -136495,7 +136709,7 @@ bt 0zdDF(=2GF`ȈQג endstream endobj -1342 0 obj +1350 0 obj << /Filter /FlateDecode /Type /XObject @@ -136515,7 +136729,7 @@ stream x1 0gB*ؽpm9lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lpرcAփ[ aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lĎ ða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  vX` Fp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða؀ڱcAփ[aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68 7 endstream endobj -1343 0 obj +1351 0 obj << /Type /XObject /Subtype /Image @@ -136530,7 +136744,7 @@ endobj /Columns 4961 /Colors 3 >> -/SMask 1344 0 R +/SMask 1352 0 R /Length 3104347 >> stream @@ -148389,7 +148603,7 @@ V* X6::rškJ> -/SMask 1346 0 R +/SMask 1354 0 R /Length 3065065 >> stream @@ -159893,7 +160107,7 @@ y {Ȉ?U+ endstream endobj -1346 0 obj +1354 0 obj << /Filter /FlateDecode /Type /XObject @@ -159913,7 +160127,7 @@ stream x1 0gB*ؽpm9lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lpرcAփ[ aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lĎ ða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  vX` Fp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða؀ڱcAփ[aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68 7 endstream endobj -1347 0 obj +1355 0 obj << /Type /XObject /Subtype /Image @@ -159928,7 +160142,7 @@ endobj /Columns 4961 /Colors 3 >> -/SMask 1348 0 R +/SMask 1356 0 R /Length 4740001 >> stream @@ -177814,7 +178028,7 @@ X 3=e1'ࡹOmΔEAџɝ5yBWژwf܋{O?pyztkϽџћ#r#,μ'{7^Nj;}'iLN^rv{>㇏mUls0Ԯ/=_;xhwwŒe8;ua?wj/Xx~j$돈GNiW ɩC㇏4oVOK33ӗL_މߥO>RVޓ쾴ys%UvZ> -/SMask 1350 0 R +/SMask 1358 0 R /Length 281568 >> stream @@ -178687,7 +178901,7 @@ O^ AAfiii5  rigӹ\òez(8sjqD}}ȑ#U9 heAdQifιmW̲ Ad@+   ,2^   \heAAAEZYAAAdVAAAYdEAAAheAAAEZYAAAdVAAAYdEAAAheAAAEZYAAAd M7- endstream endobj -1350 0 obj +1358 0 obj << /Filter /FlateDecode /Type /XObject @@ -178708,7 +178922,7 @@ x zu(8ް; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; "; ".{ endstream endobj -1351 0 obj +1359 0 obj << /Type /XObject /Subtype /Image @@ -178723,7 +178937,7 @@ endobj /Columns 4961 /Colors 3 >> -/SMask 1352 0 R +/SMask 1360 0 R /Length 4818808 >> stream @@ -196496,7 +196710,7 @@ t&o 71 endstream endobj -1352 0 obj +1360 0 obj << /Filter /FlateDecode /Type /XObject @@ -196516,7 +196730,7 @@ stream x1 0gB*ؽpm9lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lP8lpرcAփ[ aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lĎ ða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  vX` Fp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða؀ڱcAփ[aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68  aða0lp68 7 endstream endobj -1353 0 obj +1361 0 obj << /Type /XObject /Subtype /Image @@ -196531,7 +196745,7 @@ endobj /Columns 4969 /Colors 3 >> -/SMask 1354 0 R +/SMask 1362 0 R /Length 4512504 >> stream @@ -213375,7 +213589,7 @@ V ]~ʞ&[oߧVQ+-LhOG-}WNq;Z#vWO!˯sS3Rdj;jes9)WcW)n'_kĮJ)v{s*{Fl}ZmG3v>=[jJ^9kص{^)>.v}NeH]S}&tпz#_ؾ^]+|vs+֧ϩk}jҲD9Woqzb ׫+y5b}xR=9=#uM޾OVZH;gўB-XO[ az5v%vFڽ[Bj_>gvJ>is:S_i/l_ƮSNֈ]9bSHkT#Z endstream endobj -1354 0 obj +1362 0 obj << /Filter /FlateDecode /Type /XObject @@ -213633,1368 +213847,1376 @@ mp5 endstream endobj xref -0 1355 +0 1363 0000000000 65535 f 0000000015 00000 n 0000001474 00000 n 0000001630 00000 n -0000010120 00000 n -0000016448 00000 n -0000016961 00000 n -0000017160 00000 n -0000065939 00000 n -0000066898 00000 n -0000067050 00000 n -0000067202 00000 n -0000067356 00000 n -0000067497 00000 n -0000067637 00000 n -0000067779 00000 n -0000067926 00000 n -0000068072 00000 n -0000068220 00000 n -0000068386 00000 n -0000068551 00000 n -0000068718 00000 n -0000068868 00000 n -0000069017 00000 n -0000069168 00000 n -0000069329 00000 n -0000069489 00000 n -0000069651 00000 n -0000069801 00000 n -0000069950 00000 n -0000070101 00000 n -0000070247 00000 n -0000070392 00000 n -0000070539 00000 n -0000070673 00000 n -0000070806 00000 n -0000070941 00000 n -0000071103 00000 n -0000071264 00000 n -0000071427 00000 n -0000071577 00000 n -0000071726 00000 n -0000071877 00000 n -0000072016 00000 n -0000072154 00000 n -0000072294 00000 n -0000072457 00000 n -0000072619 00000 n -0000072783 00000 n -0000072936 00000 n -0000073088 00000 n -0000073242 00000 n -0000073393 00000 n -0000073543 00000 n -0000073695 00000 n -0000073851 00000 n -0000074006 00000 n -0000074163 00000 n -0000074324 00000 n -0000074484 00000 n -0000074646 00000 n -0000074813 00000 n -0000074979 00000 n -0000075147 00000 n -0000075294 00000 n -0000075440 00000 n -0000075588 00000 n -0000075741 00000 n -0000075893 00000 n -0000076047 00000 n -0000076197 00000 n -0000076346 00000 n -0000076497 00000 n -0000076646 00000 n -0000076794 00000 n -0000076944 00000 n -0000077089 00000 n -0000077233 00000 n -0000077379 00000 n -0000077524 00000 n -0000077668 00000 n -0000077814 00000 n -0000077962 00000 n -0000078109 00000 n -0000078258 00000 n -0000078403 00000 n -0000078547 00000 n -0000078693 00000 n -0000078846 00000 n -0000078998 00000 n -0000079152 00000 n -0000079292 00000 n -0000079431 00000 n -0000079572 00000 n -0000079723 00000 n -0000079873 00000 n -0000080025 00000 n -0000080176 00000 n -0000080326 00000 n -0000080478 00000 n -0000080623 00000 n -0000080768 00000 n -0000080915 00000 n -0000081067 00000 n -0000081218 00000 n -0000081371 00000 n -0000081526 00000 n -0000081680 00000 n -0000081836 00000 n -0000081985 00000 n -0000082133 00000 n -0000082283 00000 n -0000082431 00000 n -0000082577 00000 n -0000082725 00000 n -0000114122 00000 n -0000114889 00000 n -0000115050 00000 n -0000115210 00000 n -0000115372 00000 n -0000115524 00000 n -0000115675 00000 n -0000115828 00000 n -0000115977 00000 n -0000116125 00000 n -0000116275 00000 n -0000116424 00000 n -0000116572 00000 n -0000116722 00000 n -0000116858 00000 n -0000116993 00000 n -0000117130 00000 n -0000117301 00000 n -0000117471 00000 n -0000117643 00000 n -0000117826 00000 n -0000118008 00000 n -0000118192 00000 n -0000118369 00000 n -0000118545 00000 n -0000118723 00000 n -0000118884 00000 n -0000119044 00000 n -0000119206 00000 n -0000119357 00000 n -0000119507 00000 n -0000119659 00000 n -0000119808 00000 n -0000119956 00000 n -0000120106 00000 n -0000120263 00000 n -0000120419 00000 n -0000120577 00000 n -0000120752 00000 n -0000120926 00000 n -0000121102 00000 n -0000121275 00000 n -0000121447 00000 n -0000121621 00000 n -0000121768 00000 n -0000121914 00000 n -0000122062 00000 n -0000122211 00000 n -0000122359 00000 n -0000122509 00000 n -0000122654 00000 n -0000122798 00000 n -0000122944 00000 n -0000123111 00000 n -0000123277 00000 n -0000123445 00000 n -0000123614 00000 n -0000123782 00000 n -0000123952 00000 n -0000124123 00000 n -0000124293 00000 n -0000124465 00000 n -0000124618 00000 n -0000124770 00000 n -0000124924 00000 n -0000125077 00000 n -0000125229 00000 n -0000125383 00000 n -0000125554 00000 n -0000125724 00000 n -0000125896 00000 n -0000128992 00000 n -0000129215 00000 n -0000129399 00000 n -0000129865 00000 n -0000130088 00000 n -0000130272 00000 n -0000134663 00000 n -0000134910 00000 n -0000135096 00000 n -0000135288 00000 n -0000135481 00000 n -0000135675 00000 n -0000141534 00000 n -0000141737 00000 n -0000146469 00000 n -0000146692 00000 n -0000146889 00000 n -0000148571 00000 n -0000148802 00000 n -0000149000 00000 n -0000149199 00000 n -0000153415 00000 n -0000153654 00000 n -0000153839 00000 n -0000154025 00000 n -0000154212 00000 n -0000162570 00000 n -0000162773 00000 n -0000165834 00000 n -0000166037 00000 n -0000169663 00000 n -0000169866 00000 n -0000178124 00000 n -0000178327 00000 n -0000179867 00000 n -0000180070 00000 n -0000182534 00000 n -0000182773 00000 n -0000182965 00000 n -0000183158 00000 n -0000183352 00000 n -0000186725 00000 n -0000186964 00000 n -0000187154 00000 n -0000187345 00000 n -0000187537 00000 n -0000190383 00000 n -0000190622 00000 n -0000190811 00000 n -0000191001 00000 n -0000191192 00000 n -0000194198 00000 n -0000194421 00000 n -0000194611 00000 n -0000195331 00000 n -0000195562 00000 n -0000195755 00000 n -0000195949 00000 n -0000197883 00000 n -0000198122 00000 n -0000198320 00000 n -0000198519 00000 n -0000198719 00000 n -0000200317 00000 n -0000200540 00000 n -0000200744 00000 n -0000201418 00000 n -0000201657 00000 n -0000201862 00000 n -0000202068 00000 n -0000202259 00000 n -0000203288 00000 n -0000203639 00000 n -0000203833 00000 n -0000204028 00000 n -0000204223 00000 n -0000204419 00000 n -0000204616 00000 n -0000204815 00000 n -0000205014 00000 n -0000205215 00000 n -0000205410 00000 n -0000205606 00000 n -0000205803 00000 n -0000206001 00000 n -0000206200 00000 n -0000206400 00000 n -0000206601 00000 n -0000206803 00000 n -0000207006 00000 n -0000209646 00000 n -0000209885 00000 n -0000210072 00000 n -0000210259 00000 n -0000210446 00000 n -0000211404 00000 n -0000211675 00000 n -0000211859 00000 n -0000212044 00000 n -0000212230 00000 n -0000212415 00000 n -0000212601 00000 n -0000212788 00000 n -0000212975 00000 n -0000213969 00000 n -0000214224 00000 n -0000214414 00000 n -0000214605 00000 n -0000214788 00000 n -0000214972 00000 n -0000215157 00000 n -0000217506 00000 n -0000217825 00000 n -0000218017 00000 n -0000218210 00000 n -0000218404 00000 n -0000218604 00000 n -0000218805 00000 n -0000219007 00000 n -0000219207 00000 n -0000219408 00000 n -0000219610 00000 n -0000219805 00000 n -0000220001 00000 n -0000220198 00000 n -0000220397 00000 n -0000223102 00000 n -0000223357 00000 n -0000223557 00000 n -0000223758 00000 n -0000223955 00000 n -0000224153 00000 n -0000224351 00000 n -0000226468 00000 n -0000226763 00000 n -0000226969 00000 n -0000227176 00000 n -0000227384 00000 n -0000227584 00000 n -0000227785 00000 n -0000227987 00000 n -0000228185 00000 n -0000228384 00000 n -0000228584 00000 n -0000228784 00000 n -0000231028 00000 n -0000231283 00000 n -0000231486 00000 n -0000231690 00000 n -0000231887 00000 n -0000232085 00000 n -0000232284 00000 n -0000233819 00000 n -0000234130 00000 n -0000234312 00000 n -0000234495 00000 n -0000234679 00000 n -0000234873 00000 n -0000235068 00000 n -0000235264 00000 n -0000235458 00000 n -0000235653 00000 n -0000235849 00000 n -0000236043 00000 n -0000236238 00000 n -0000236434 00000 n -0000238963 00000 n -0000239258 00000 n -0000239452 00000 n -0000239647 00000 n -0000239843 00000 n -0000240033 00000 n -0000240224 00000 n -0000240416 00000 n -0000240609 00000 n -0000240802 00000 n -0000240997 00000 n -0000241185 00000 n -0000242085 00000 n -0000242412 00000 n -0000242603 00000 n -0000242795 00000 n -0000242987 00000 n -0000243180 00000 n -0000243374 00000 n -0000243564 00000 n -0000243755 00000 n -0000243947 00000 n -0000244145 00000 n -0000244344 00000 n -0000244544 00000 n -0000244733 00000 n -0000244923 00000 n -0000245114 00000 n -0000247397 00000 n -0000247716 00000 n -0000247906 00000 n -0000248096 00000 n -0000248288 00000 n -0000248481 00000 n -0000248675 00000 n -0000248870 00000 n -0000249079 00000 n -0000249288 00000 n -0000249499 00000 n -0000249692 00000 n -0000249885 00000 n -0000250080 00000 n -0000250271 00000 n -0000252176 00000 n -0000252431 00000 n -0000252625 00000 n -0000252820 00000 n -0000253011 00000 n -0000253203 00000 n -0000253396 00000 n -0000255250 00000 n -0000255497 00000 n -0000255694 00000 n -0000255892 00000 n -0000256091 00000 n -0000256282 00000 n -0000257128 00000 n -0000257359 00000 n -0000257551 00000 n -0000257744 00000 n -0000260128 00000 n -0000260331 00000 n -0000268304 00000 n -0000268507 00000 n -0000272807 00000 n -0000273062 00000 n -0000273307 00000 n -0000273547 00000 n -0000273742 00000 n -0000273938 00000 n -0000274135 00000 n -0000279665 00000 n -0000279888 00000 n -0000280085 00000 n -0000281766 00000 n -0000281997 00000 n -0000282195 00000 n -0000282394 00000 n -0000286593 00000 n -0000286832 00000 n -0000287017 00000 n -0000287203 00000 n -0000287390 00000 n -0000292193 00000 n -0000292396 00000 n -0000296804 00000 n -0000297007 00000 n -0000301445 00000 n -0000301684 00000 n -0000301875 00000 n -0000302067 00000 n -0000302260 00000 n -0000302719 00000 n -0000302922 00000 n -0000303396 00000 n -0000303599 00000 n -0000304059 00000 n -0000304262 00000 n -0000306945 00000 n -0000307216 00000 n -0000307407 00000 n -0000307599 00000 n -0000307792 00000 n -0000307983 00000 n -0000308175 00000 n -0000308368 00000 n -0000308557 00000 n -0000311286 00000 n -0000311525 00000 n -0000311717 00000 n -0000311910 00000 n -0000312104 00000 n -0000312804 00000 n -0000313043 00000 n -0000313238 00000 n -0000313434 00000 n -0000313628 00000 n -0000314436 00000 n -0000314675 00000 n -0000314870 00000 n -0000315066 00000 n -0000315260 00000 n -0000316071 00000 n -0000316310 00000 n -0000316505 00000 n -0000316701 00000 n -0000316890 00000 n -0000317715 00000 n -0000317978 00000 n -0000318168 00000 n -0000318359 00000 n -0000318549 00000 n -0000318740 00000 n -0000318932 00000 n -0000319126 00000 n -0000320637 00000 n -0000320868 00000 n -0000321063 00000 n -0000321259 00000 n -0000322617 00000 n -0000322856 00000 n -0000323050 00000 n -0000323244 00000 n -0000323438 00000 n -0000324504 00000 n -0000324751 00000 n -0000324945 00000 n -0000325140 00000 n -0000325336 00000 n -0000325530 00000 n -0000326783 00000 n -0000327022 00000 n -0000327217 00000 n -0000327413 00000 n -0000327607 00000 n -0000328554 00000 n -0000328793 00000 n -0000328990 00000 n -0000329188 00000 n -0000329384 00000 n -0000330256 00000 n -0000330495 00000 n -0000330692 00000 n -0000330890 00000 n -0000331086 00000 n -0000332327 00000 n -0000332558 00000 n -0000332755 00000 n -0000332953 00000 n +0000010252 00000 n +0000016580 00000 n +0000017093 00000 n +0000017292 00000 n +0000066162 00000 n +0000067121 00000 n +0000067273 00000 n +0000067425 00000 n +0000067579 00000 n +0000067720 00000 n +0000067860 00000 n +0000068002 00000 n +0000068149 00000 n +0000068295 00000 n +0000068443 00000 n +0000068609 00000 n +0000068774 00000 n +0000068941 00000 n +0000069091 00000 n +0000069240 00000 n +0000069391 00000 n +0000069552 00000 n +0000069712 00000 n +0000069874 00000 n +0000070024 00000 n +0000070173 00000 n +0000070324 00000 n +0000070470 00000 n +0000070615 00000 n +0000070762 00000 n +0000070896 00000 n +0000071029 00000 n +0000071164 00000 n +0000071326 00000 n +0000071487 00000 n +0000071650 00000 n +0000071804 00000 n +0000071957 00000 n +0000072112 00000 n +0000072258 00000 n +0000072403 00000 n +0000072550 00000 n +0000072700 00000 n +0000072849 00000 n +0000073000 00000 n +0000073139 00000 n +0000073277 00000 n +0000073417 00000 n +0000073580 00000 n +0000073742 00000 n +0000073906 00000 n +0000074059 00000 n +0000074211 00000 n +0000074365 00000 n +0000074516 00000 n +0000074666 00000 n +0000074818 00000 n +0000074974 00000 n +0000075129 00000 n +0000075286 00000 n +0000075447 00000 n +0000075607 00000 n +0000075769 00000 n +0000075936 00000 n +0000076102 00000 n +0000076270 00000 n +0000076417 00000 n +0000076563 00000 n +0000076711 00000 n +0000076864 00000 n +0000077016 00000 n +0000077170 00000 n +0000077320 00000 n +0000077469 00000 n +0000077620 00000 n +0000077769 00000 n +0000077917 00000 n +0000078067 00000 n +0000078212 00000 n +0000078356 00000 n +0000078502 00000 n +0000078647 00000 n +0000078791 00000 n +0000078937 00000 n +0000079085 00000 n +0000079232 00000 n +0000079381 00000 n +0000079526 00000 n +0000079670 00000 n +0000079816 00000 n +0000079969 00000 n +0000080121 00000 n +0000080275 00000 n +0000080415 00000 n +0000080554 00000 n +0000080695 00000 n +0000080846 00000 n +0000080997 00000 n +0000081150 00000 n +0000081302 00000 n +0000081453 00000 n +0000081606 00000 n +0000081752 00000 n +0000081897 00000 n +0000082044 00000 n +0000082196 00000 n +0000082347 00000 n +0000082500 00000 n +0000082654 00000 n +0000082806 00000 n +0000082960 00000 n +0000116643 00000 n +0000117458 00000 n +0000117607 00000 n +0000117755 00000 n +0000117905 00000 n +0000118054 00000 n +0000118202 00000 n +0000118352 00000 n +0000118513 00000 n +0000118673 00000 n +0000118835 00000 n +0000118987 00000 n +0000119138 00000 n +0000119291 00000 n +0000119440 00000 n +0000119588 00000 n +0000119738 00000 n +0000119887 00000 n +0000120035 00000 n +0000120185 00000 n +0000120321 00000 n +0000120456 00000 n +0000120593 00000 n +0000120764 00000 n +0000120934 00000 n +0000121106 00000 n +0000121289 00000 n +0000121471 00000 n +0000121655 00000 n +0000121832 00000 n +0000122008 00000 n +0000122186 00000 n +0000122347 00000 n +0000122507 00000 n +0000122669 00000 n +0000122820 00000 n +0000122970 00000 n +0000123122 00000 n +0000123271 00000 n +0000123419 00000 n +0000123569 00000 n +0000123726 00000 n +0000123882 00000 n +0000124040 00000 n +0000124215 00000 n +0000124389 00000 n +0000124565 00000 n +0000124738 00000 n +0000124910 00000 n +0000125084 00000 n +0000125231 00000 n +0000125377 00000 n +0000125525 00000 n +0000125674 00000 n +0000125822 00000 n +0000125972 00000 n +0000126117 00000 n +0000126261 00000 n +0000126407 00000 n +0000126574 00000 n +0000126740 00000 n +0000126908 00000 n +0000127077 00000 n +0000127245 00000 n +0000127415 00000 n +0000127586 00000 n +0000127756 00000 n +0000127928 00000 n +0000128081 00000 n +0000128233 00000 n +0000128387 00000 n +0000128540 00000 n +0000128692 00000 n +0000128846 00000 n +0000129017 00000 n +0000129187 00000 n +0000129359 00000 n +0000132455 00000 n +0000132678 00000 n +0000132862 00000 n +0000133328 00000 n +0000133551 00000 n +0000133735 00000 n +0000138145 00000 n +0000138392 00000 n +0000138578 00000 n +0000138770 00000 n +0000138963 00000 n +0000139157 00000 n +0000145242 00000 n +0000145445 00000 n +0000150459 00000 n +0000150682 00000 n +0000150879 00000 n +0000152558 00000 n +0000152789 00000 n +0000152987 00000 n +0000153186 00000 n +0000157425 00000 n +0000157664 00000 n +0000157849 00000 n +0000158035 00000 n +0000158222 00000 n +0000166580 00000 n +0000166783 00000 n +0000169844 00000 n +0000170047 00000 n +0000173673 00000 n +0000173876 00000 n +0000182134 00000 n +0000182337 00000 n +0000183899 00000 n +0000184102 00000 n +0000186566 00000 n +0000186805 00000 n +0000186997 00000 n +0000187190 00000 n +0000187384 00000 n +0000190757 00000 n +0000190996 00000 n +0000191186 00000 n +0000191377 00000 n +0000191569 00000 n +0000194410 00000 n +0000194649 00000 n +0000194838 00000 n +0000195028 00000 n +0000195219 00000 n +0000198225 00000 n +0000198448 00000 n +0000198638 00000 n +0000199358 00000 n +0000199589 00000 n +0000199782 00000 n +0000199976 00000 n +0000201901 00000 n +0000202140 00000 n +0000202338 00000 n +0000202537 00000 n +0000202737 00000 n +0000204335 00000 n +0000204558 00000 n +0000204762 00000 n +0000205469 00000 n +0000205708 00000 n +0000205913 00000 n +0000206119 00000 n +0000206310 00000 n +0000207390 00000 n +0000207741 00000 n +0000207935 00000 n +0000208130 00000 n +0000208325 00000 n +0000208521 00000 n +0000208718 00000 n +0000208917 00000 n +0000209116 00000 n +0000209317 00000 n +0000209512 00000 n +0000209708 00000 n +0000209905 00000 n +0000210103 00000 n +0000210302 00000 n +0000210502 00000 n +0000210703 00000 n +0000210905 00000 n +0000211108 00000 n +0000213748 00000 n +0000213987 00000 n +0000214174 00000 n +0000214361 00000 n +0000214548 00000 n +0000215544 00000 n +0000215815 00000 n +0000215999 00000 n +0000216184 00000 n +0000216370 00000 n +0000216555 00000 n +0000216741 00000 n +0000216928 00000 n +0000217115 00000 n +0000218137 00000 n +0000218392 00000 n +0000218582 00000 n +0000218773 00000 n +0000218956 00000 n +0000219140 00000 n +0000219325 00000 n +0000221690 00000 n +0000222009 00000 n +0000222201 00000 n +0000222394 00000 n +0000222588 00000 n +0000222788 00000 n +0000222989 00000 n +0000223191 00000 n +0000223391 00000 n +0000223592 00000 n +0000223794 00000 n +0000223989 00000 n +0000224185 00000 n +0000224382 00000 n +0000224581 00000 n +0000227299 00000 n +0000227554 00000 n +0000227754 00000 n +0000227955 00000 n +0000228152 00000 n +0000228350 00000 n +0000228548 00000 n +0000230711 00000 n +0000231006 00000 n +0000231212 00000 n +0000231419 00000 n +0000231627 00000 n +0000231827 00000 n +0000232028 00000 n +0000232230 00000 n +0000232428 00000 n +0000232627 00000 n +0000232827 00000 n +0000233027 00000 n +0000235285 00000 n +0000235540 00000 n +0000235743 00000 n +0000235947 00000 n +0000236144 00000 n +0000236342 00000 n +0000236541 00000 n +0000238089 00000 n +0000238400 00000 n +0000238582 00000 n +0000238765 00000 n +0000238949 00000 n +0000239143 00000 n +0000239338 00000 n +0000239534 00000 n +0000239728 00000 n +0000239923 00000 n +0000240119 00000 n +0000240313 00000 n +0000240508 00000 n +0000240704 00000 n +0000243242 00000 n +0000243537 00000 n +0000243731 00000 n +0000243926 00000 n +0000244122 00000 n +0000244312 00000 n +0000244503 00000 n +0000244695 00000 n +0000244888 00000 n +0000245081 00000 n +0000245276 00000 n +0000245464 00000 n +0000246379 00000 n +0000246706 00000 n +0000246897 00000 n +0000247089 00000 n +0000247281 00000 n +0000247474 00000 n +0000247668 00000 n +0000247858 00000 n +0000248049 00000 n +0000248241 00000 n +0000248439 00000 n +0000248638 00000 n +0000248838 00000 n +0000249027 00000 n +0000249217 00000 n +0000249408 00000 n +0000251737 00000 n +0000252056 00000 n +0000252246 00000 n +0000252436 00000 n +0000252628 00000 n +0000252821 00000 n +0000253015 00000 n +0000253210 00000 n +0000253419 00000 n +0000253628 00000 n +0000253839 00000 n +0000254032 00000 n +0000254225 00000 n +0000254420 00000 n +0000254611 00000 n +0000256516 00000 n +0000256771 00000 n +0000256965 00000 n +0000257160 00000 n +0000257351 00000 n +0000257543 00000 n +0000257736 00000 n +0000259599 00000 n +0000259846 00000 n +0000260043 00000 n +0000260241 00000 n +0000260440 00000 n +0000260631 00000 n +0000261507 00000 n +0000261738 00000 n +0000261930 00000 n +0000262123 00000 n +0000264536 00000 n +0000264739 00000 n +0000275551 00000 n +0000275754 00000 n +0000280054 00000 n +0000280309 00000 n +0000280554 00000 n +0000280794 00000 n +0000280989 00000 n +0000281185 00000 n +0000281382 00000 n +0000286912 00000 n +0000287135 00000 n +0000287332 00000 n +0000289013 00000 n +0000289244 00000 n +0000289442 00000 n +0000289641 00000 n +0000293840 00000 n +0000294079 00000 n +0000294264 00000 n +0000294450 00000 n +0000294637 00000 n +0000299440 00000 n +0000299643 00000 n +0000304051 00000 n +0000304254 00000 n +0000308692 00000 n +0000308931 00000 n +0000309122 00000 n +0000309314 00000 n +0000309507 00000 n +0000309966 00000 n +0000310169 00000 n +0000310643 00000 n +0000310846 00000 n +0000311306 00000 n +0000311509 00000 n +0000314192 00000 n +0000314463 00000 n +0000314654 00000 n +0000314846 00000 n +0000315039 00000 n +0000315230 00000 n +0000315422 00000 n +0000315615 00000 n +0000315804 00000 n +0000318533 00000 n +0000318772 00000 n +0000318964 00000 n +0000319157 00000 n +0000319351 00000 n +0000320051 00000 n +0000320290 00000 n +0000320485 00000 n +0000320681 00000 n +0000320875 00000 n +0000321683 00000 n +0000321922 00000 n +0000322117 00000 n +0000322313 00000 n +0000322507 00000 n +0000323318 00000 n +0000323557 00000 n +0000323752 00000 n +0000323948 00000 n +0000324137 00000 n +0000324962 00000 n +0000325225 00000 n +0000325415 00000 n +0000325606 00000 n +0000325796 00000 n +0000325987 00000 n +0000326179 00000 n +0000326373 00000 n +0000327884 00000 n +0000328115 00000 n +0000328310 00000 n +0000328506 00000 n +0000329864 00000 n +0000330103 00000 n +0000330297 00000 n +0000330491 00000 n +0000330685 00000 n +0000331751 00000 n +0000331998 00000 n +0000332192 00000 n +0000332387 00000 n +0000332583 00000 n +0000332777 00000 n +0000334030 00000 n +0000334269 00000 n +0000334464 00000 n 0000334660 00000 n -0000334923 00000 n -0000335121 00000 n -0000335320 00000 n -0000335520 00000 n -0000335718 00000 n -0000335917 00000 n -0000336117 00000 n +0000334854 00000 n +0000335801 00000 n +0000336040 00000 n +0000336237 00000 n +0000336435 00000 n +0000336631 00000 n +0000337503 00000 n +0000337742 00000 n +0000337939 00000 n +0000338137 00000 n 0000338333 00000 n -0000338580 00000 n -0000338778 00000 n -0000338977 00000 n -0000339177 00000 n -0000339381 00000 n -0000340260 00000 n -0000340491 00000 n -0000340696 00000 n -0000340902 00000 n -0000343490 00000 n -0000343693 00000 n -0000346167 00000 n -0000346414 00000 n -0000346608 00000 n -0000346803 00000 n -0000346999 00000 n -0000347193 00000 n -0000348172 00000 n -0000348435 00000 n -0000348630 00000 n -0000348826 00000 n -0000349020 00000 n -0000349215 00000 n -0000349411 00000 n -0000349605 00000 n -0000350475 00000 n -0000350738 00000 n -0000350933 00000 n -0000351129 00000 n -0000351323 00000 n -0000351518 00000 n -0000351714 00000 n -0000351906 00000 n -0000354353 00000 n -0000354608 00000 n -0000354801 00000 n -0000354995 00000 n -0000355180 00000 n -0000355366 00000 n -0000355553 00000 n -0000357603 00000 n -0000357874 00000 n -0000358062 00000 n -0000358251 00000 n -0000358441 00000 n -0000358629 00000 n -0000358818 00000 n -0000359008 00000 n -0000359202 00000 n -0000360234 00000 n -0000360489 00000 n -0000360684 00000 n -0000360880 00000 n -0000361069 00000 n -0000361259 00000 n -0000361450 00000 n -0000363428 00000 n -0000363675 00000 n -0000363869 00000 n -0000364064 00000 n -0000364260 00000 n -0000364454 00000 n -0000365974 00000 n -0000366229 00000 n -0000366424 00000 n -0000366620 00000 n -0000366814 00000 n -0000367009 00000 n -0000367205 00000 n -0000369777 00000 n -0000370000 00000 n -0000370194 00000 n -0000371597 00000 n -0000371828 00000 n -0000372023 00000 n -0000372219 00000 n -0000374815 00000 n -0000375018 00000 n -0000376954 00000 n -0000377249 00000 n -0000377443 00000 n -0000377638 00000 n -0000377834 00000 n -0000378026 00000 n -0000378219 00000 n -0000378413 00000 n -0000378607 00000 n -0000378802 00000 n -0000378998 00000 n -0000379193 00000 n -0000380048 00000 n -0000380287 00000 n -0000380483 00000 n -0000380680 00000 n -0000380872 00000 n -0000382198 00000 n -0000382429 00000 n -0000382622 00000 n -0000382816 00000 n -0000386046 00000 n -0000386249 00000 n -0000389401 00000 n -0000389604 00000 n -0000391320 00000 n -0000391639 00000 n -0000391822 00000 n -0000392006 00000 n -0000392191 00000 n -0000392374 00000 n -0000392558 00000 n -0000392743 00000 n -0000392926 00000 n -0000393110 00000 n -0000393295 00000 n -0000393483 00000 n -0000393672 00000 n -0000393862 00000 n -0000394049 00000 n -0000395248 00000 n -0000395559 00000 n -0000395748 00000 n -0000395938 00000 n -0000396133 00000 n -0000396329 00000 n -0000396526 00000 n -0000396713 00000 n -0000396901 00000 n -0000397090 00000 n -0000397279 00000 n -0000397469 00000 n -0000397660 00000 n -0000397845 00000 n -0000399680 00000 n -0000399959 00000 n -0000400145 00000 n -0000400332 00000 n -0000400520 00000 n -0000400709 00000 n -0000400899 00000 n -0000401092 00000 n -0000401286 00000 n -0000401481 00000 n -0000404331 00000 n -0000404534 00000 n -0000406831 00000 n -0000407102 00000 n -0000407288 00000 n -0000407474 00000 n -0000407662 00000 n -0000407847 00000 n -0000408032 00000 n -0000408219 00000 n -0000408410 00000 n -0000409992 00000 n -0000410231 00000 n -0000410423 00000 n -0000410616 00000 n -0000410805 00000 n -0000411931 00000 n -0000412170 00000 n -0000412360 00000 n -0000412551 00000 n -0000412739 00000 n -0000414145 00000 n -0000414376 00000 n -0000414565 00000 n -0000414755 00000 n -0000417036 00000 n -0000417275 00000 n -0000417470 00000 n -0000417665 00000 n -0000417861 00000 n -0000419537 00000 n -0000419832 00000 n -0000420031 00000 n -0000420230 00000 n -0000420431 00000 n -0000420621 00000 n -0000420812 00000 n -0000421004 00000 n -0000421193 00000 n -0000421383 00000 n -0000421574 00000 n -0000421762 00000 n -0000423815 00000 n -0000424046 00000 n -0000424236 00000 n -0000424427 00000 n -0000427839 00000 n -0000428042 00000 n -0000431213 00000 n -0000431416 00000 n -0000433629 00000 n -0000433852 00000 n -0000434036 00000 n -0000434477 00000 n -0000434700 00000 n -0000434884 00000 n -0000435570 00000 n -0000435801 00000 n -0000435987 00000 n -0000436171 00000 n -0000436610 00000 n -0000436833 00000 n -0000437017 00000 n -0000440689 00000 n -0000440912 00000 n -0000441098 00000 n -0000445078 00000 n -0000445281 00000 n -0000448815 00000 n -0000449018 00000 n -0000452453 00000 n -0000452676 00000 n -0000452861 00000 n -0000454432 00000 n -0000454695 00000 n -0000454883 00000 n -0000455072 00000 n -0000455262 00000 n -0000455453 00000 n -0000455645 00000 n -0000455835 00000 n -0000458094 00000 n -0000458373 00000 n -0000458564 00000 n -0000458756 00000 n -0000458952 00000 n -0000459149 00000 n -0000459347 00000 n -0000459543 00000 n -0000459740 00000 n -0000459938 00000 n -0000462991 00000 n -0000463214 00000 n -0000463408 00000 n -0000465435 00000 n -0000465674 00000 n -0000465869 00000 n -0000466065 00000 n -0000466254 00000 n -0000466785 00000 n -0000467024 00000 n -0000467214 00000 n -0000467405 00000 n -0000467593 00000 n -0000468800 00000 n -0000469031 00000 n -0000469220 00000 n -0000469410 00000 n -0000472799 00000 n -0000473002 00000 n -0000476869 00000 n -0000477072 00000 n -0000481141 00000 n -0000481344 00000 n -0000482647 00000 n -0000482894 00000 n -0000483087 00000 n -0000483281 00000 n -0000483476 00000 n -0000483670 00000 n -0000484870 00000 n -0000485109 00000 n -0000485304 00000 n -0000485500 00000 n -0000485694 00000 n -0000486247 00000 n -0000486478 00000 n -0000486673 00000 n -0000486869 00000 n -0000489095 00000 n -0000489334 00000 n -0000489527 00000 n -0000489721 00000 n -0000489916 00000 n -0000492545 00000 n -0000492784 00000 n -0000492973 00000 n -0000493163 00000 n -0000493354 00000 n -0000494606 00000 n -0000494829 00000 n -0000495013 00000 n -0000495468 00000 n -0000495691 00000 n -0000495875 00000 n -0000496644 00000 n -0000496875 00000 n -0000497061 00000 n -0000497245 00000 n -0000497701 00000 n -0000497924 00000 n -0000498108 00000 n -0000498737 00000 n -0000498968 00000 n -0000499154 00000 n -0000499338 00000 n -0000499795 00000 n -0000500018 00000 n -0000500202 00000 n -0000500940 00000 n -0000501171 00000 n -0000501357 00000 n -0000501541 00000 n -0000501998 00000 n -0000502221 00000 n -0000502405 00000 n -0000503132 00000 n -0000503363 00000 n -0000503549 00000 n -0000503733 00000 n -0000504191 00000 n -0000504414 00000 n -0000504598 00000 n -0000506671 00000 n -0000506918 00000 n -0000507104 00000 n -0000507293 00000 n -0000507483 00000 n -0000507674 00000 n -0000510123 00000 n -0000510346 00000 n -0000510530 00000 n -0000510983 00000 n -0000511206 00000 n -0000511390 00000 n -0000511961 00000 n -0000512192 00000 n -0000512378 00000 n -0000512562 00000 n -0000513011 00000 n -0000513234 00000 n -0000513418 00000 n -0000513877 00000 n -0000514100 00000 n -0000514286 00000 n -0000516891 00000 n -0000517094 00000 n -0000519973 00000 n -0000520176 00000 n -0000521240 00000 n -0000521443 00000 n -0000523143 00000 n -0000523346 00000 n -0000525195 00000 n -0000525398 00000 n -0000528578 00000 n -0000528781 00000 n -0000531534 00000 n -0000531737 00000 n -0000534490 00000 n -0000534693 00000 n -0000535466 00000 n -0000535669 00000 n -0000536973 00000 n -0000537176 00000 n -0000540547 00000 n -0000540750 00000 n -0000543770 00000 n -0000543973 00000 n -0000546995 00000 n -0000547198 00000 n -0000548586 00000 n -0000548789 00000 n -0000551532 00000 n -0000551735 00000 n -0000552572 00000 n -0000552775 00000 n -0000554471 00000 n -0000554674 00000 n -0000556406 00000 n -0000556609 00000 n -0000558023 00000 n -0000558226 00000 n -0000561103 00000 n -0000561306 00000 n -0000564322 00000 n -0000564525 00000 n -0000566449 00000 n -0000566652 00000 n -0000569584 00000 n -0000569787 00000 n -0000572292 00000 n -0000572495 00000 n -0000574137 00000 n -0000574340 00000 n -0000577211 00000 n -0000577414 00000 n -0000578247 00000 n -0000578450 00000 n -0000580846 00000 n -0000581049 00000 n -0000584428 00000 n -0000584631 00000 n -0000586709 00000 n -0000586912 00000 n -0000590029 00000 n -0000590232 00000 n -0000592774 00000 n -0000592977 00000 n -0000595410 00000 n -0000595613 00000 n -0000598286 00000 n -0000598489 00000 n -0000600989 00000 n -0000601192 00000 n -0000603215 00000 n -0000603418 00000 n -0000605836 00000 n -0000606039 00000 n -0000608223 00000 n -0000608426 00000 n -0000610683 00000 n -0000610886 00000 n -0000613368 00000 n -0000613571 00000 n -0000615205 00000 n -0000615408 00000 n -0000618536 00000 n -0000618739 00000 n -0000618882 00000 n -0000619075 00000 n -0000619238 00000 n -0000619408 00000 n -0000619554 00000 n -0000619699 00000 n -0000619882 00000 n -0000620012 00000 n -0000620216 00000 n -0000620371 00000 n -0000620605 00000 n -0000620905 00000 n -0000621171 00000 n -0000621357 00000 n -0000621506 00000 n -0000621786 00000 n -0000621949 00000 n -0000622175 00000 n -0000622352 00000 n -0000622525 00000 n -0000622702 00000 n -0000622863 00000 n -0000623010 00000 n -0000623173 00000 n -0000623475 00000 n -0000623634 00000 n -0000623934 00000 n -0000624102 00000 n -0000624258 00000 n -0000624407 00000 n -0000624544 00000 n -0000624684 00000 n -0000624964 00000 n -0000625121 00000 n -0000625306 00000 n -0000625573 00000 n -0000625706 00000 n -0000625861 00000 n -0000626089 00000 n -0000626317 00000 n -0000626446 00000 n -0000626616 00000 n -0000626841 00000 n -0000626995 00000 n -0000627118 00000 n -0000627258 00000 n -0000627395 00000 n -0000627530 00000 n -0000627682 00000 n -0000627947 00000 n -0000628189 00000 n -0000628436 00000 n -0000628628 00000 n -0000628771 00000 n -0000629058 00000 n -0000629301 00000 n -0000629436 00000 n -0000629623 00000 n -0000629756 00000 n -0000629902 00000 n -0000630102 00000 n -0000630259 00000 n -0000630488 00000 n -0000630674 00000 n -0000630820 00000 n -0000630978 00000 n -0000631137 00000 n -0000631290 00000 n -0000631443 00000 n -0000631599 00000 n -0000631752 00000 n -0000631913 00000 n -0000632061 00000 n -0000632221 00000 n -0000632381 00000 n -0000632535 00000 n -0000632695 00000 n -0000632858 00000 n -0000633015 00000 n -0000633172 00000 n -0000633341 00000 n -0000633501 00000 n -0000633658 00000 n -0000633800 00000 n -0000633960 00000 n -0000634105 00000 n -0000634271 00000 n -0000634434 00000 n -0000634589 00000 n -0000634739 00000 n -0000634888 00000 n -0000635041 00000 n -0000635203 00000 n -0000635364 00000 n -0000635513 00000 n -0000635663 00000 n -0000635811 00000 n -0000635970 00000 n -0000636130 00000 n -0000636291 00000 n -0000636443 00000 n -0000636595 00000 n -0000636741 00000 n -0000636806 00000 n -0000641163 00000 n -0000649621 00000 n -0000654071 00000 n -0000657535 00000 n -0000663631 00000 n -0000664418 00000 n -0000665651 00000 n -0000665883 00000 n -0000666481 00000 n -0000666635 00000 n -0000668553 00000 n -0000668801 00000 n -0000669653 00000 n -0000669816 00000 n -0000671021 00000 n -0000671253 00000 n -0000671792 00000 n -0000671947 00000 n -0000672913 00000 n -0000673141 00000 n -0000673607 00000 n -0000673756 00000 n -0000675437 00000 n -0000675683 00000 n -0000676305 00000 n -0000676464 00000 n -0000676855 00000 n -0000677095 00000 n -0000677330 00000 n -0000677490 00000 n -0000677616 00000 n -0004382343 00000 n -0004452696 00000 n -0005235007 00000 n -0005239461 00000 n -0005967381 00000 n -0005971827 00000 n -0006260073 00000 n -0006264527 00000 n -0006390380 00000 n -0006422815 00000 n -0006426995 00000 n -0006428672 00000 n -0006439096 00000 n -0006441006 00000 n -0006933175 00000 n -0006935637 00000 n -0007108901 00000 n -0007155431 00000 n -0007777967 00000 n -0007780671 00000 n -0007809898 00000 n -0007810259 00000 n -0007811106 00000 n -0007811364 00000 n -0007814533 00000 n -0007814829 00000 n -0007817592 00000 n -0007817866 00000 n -0007826191 00000 n -0007826535 00000 n -0007828607 00000 n -0007829490 00000 n -0007831156 00000 n -0007831910 00000 n -0007832471 00000 n -0007833157 00000 n -0007833718 00000 n -0007834407 00000 n -0007834968 00000 n -0007835722 00000 n -0007838174 00000 n -0007838474 00000 n -0007840925 00000 n -0007841225 00000 n -0007841762 00000 n -0007842062 00000 n -0007845146 00000 n -0007845622 00000 n -0007846574 00000 n -0007846992 00000 n -0007849757 00000 n -0007850033 00000 n -0007860540 00000 n -0007863689 00000 n -0007864189 00000 n -0007864465 00000 n -0007868259 00000 n -0007868666 00000 n -0007869474 00000 n -0007869817 00000 n -0007870742 00000 n -0007871085 00000 n -0008383294 00000 n -0008410200 00000 n -0008418547 00000 n -0008418891 00000 n -0008433106 00000 n -0008433471 00000 n -0008445932 00000 n -0008446276 00000 n -0008454814 00000 n -0008455163 00000 n -0008463377 00000 n -0008463737 00000 n -0008465319 00000 n -0008465574 00000 n -0008466115 00000 n -0008466489 00000 n -0008467034 00000 n -0008467408 00000 n -0008467954 00000 n -0008468328 00000 n -0008468873 00000 n -0008469247 00000 n -0008469794 00000 n -0008470168 00000 n -0008471687 00000 n -0008471942 00000 n -0008472488 00000 n -0008472862 00000 n -0008474161 00000 n -0008474416 00000 n -0008474973 00000 n -0008475228 00000 n -0008479119 00000 n -0008479537 00000 n -0008480187 00000 n -0008480485 00000 n -0009258783 00000 n -0009261152 00000 n -0009346940 00000 n -0009348339 00000 n -0009398403 00000 n -0009672829 00000 n -0009676447 00000 n -0009794560 00000 n -0009798440 00000 n -0010993147 00000 n -0012759820 00000 n -0012771256 00000 n -0012771663 00000 n -0012777933 00000 n -0012778325 00000 n -0012783278 00000 n -0012783649 00000 n -0012933884 00000 n -0012935487 00000 n -0013050145 00000 n -0013052098 00000 n -0013173448 00000 n -0013175580 00000 n -0013179633 00000 n -0013180434 00000 n -0013186417 00000 n -0013187178 00000 n -0013306495 00000 n -0013308630 00000 n -0013417293 00000 n -0013419441 00000 n -0013523784 00000 n -0013525742 00000 n -0013630512 00000 n -0013632660 00000 n -0013752610 00000 n -0013754761 00000 n -0013863890 00000 n -0013866111 00000 n -0013976500 00000 n -0013978471 00000 n -0014059892 00000 n -0014060591 00000 n -0014078514 00000 n -0014079126 00000 n -0014618847 00000 n -0014621956 00000 n -0015137496 00000 n -0015140432 00000 n -0015673656 00000 n -0015676524 00000 n -0016216285 00000 n -0016219232 00000 n -0016758773 00000 n -0016761739 00000 n -0016781375 00000 n -0016782312 00000 n -0016810113 00000 n -0016810512 00000 n -0016812059 00000 n -0016812342 00000 n -0016813739 00000 n -0016814022 00000 n -0017071368 00000 n -0017073752 00000 n -0017095117 00000 n -0017096038 00000 n -0017603080 00000 n -0017606027 00000 n -0018145553 00000 n -0018148272 00000 n -0018692719 00000 n -0018695726 00000 n -0018726409 00000 n -0018728321 00000 n -0018753572 00000 n -0018754456 00000 n -0018769268 00000 n -0018769629 00000 n -0018786100 00000 n -0018786439 00000 n -0018805024 00000 n -0018806155 00000 n -0019127518 00000 n -0019130149 00000 n -0019130631 00000 n -0019130905 00000 n -0019131401 00000 n -0019131675 00000 n -0019132205 00000 n -0019132479 00000 n -0019133827 00000 n -0019134399 00000 n -0019135777 00000 n -0019136365 00000 n -0019139189 00000 n -0019139465 00000 n -0019141935 00000 n -0019142211 00000 n -0019142764 00000 n -0019143040 00000 n -0019152508 00000 n -0019153253 00000 n -0019154862 00000 n -0019155451 00000 n -0019156399 00000 n -0019156742 00000 n -0019158773 00000 n -0019159045 00000 n -0019161058 00000 n -0019161330 00000 n -0019171145 00000 n -0019174089 00000 n -0019215094 00000 n -0019216137 00000 n -0019217829 00000 n -0019219040 00000 n -0019227484 00000 n -0019228620 00000 n -0019228873 00000 n -0019229957 00000 n -0019230210 00000 n -0019231347 00000 n -0019231810 00000 n -0019233361 00000 n -0019233824 00000 n -0019235566 00000 n -0019236029 00000 n -0022801333 00000 n -0022850106 00000 n -0026611833 00000 n -0026660606 00000 n -0027144312 00000 n -0027148754 00000 n -0027149499 00000 n -0027149797 00000 n -0027150509 00000 n -0027150807 00000 n -0027151490 00000 n -0027151788 00000 n -0027152446 00000 n -0027152744 00000 n -0027221456 00000 n -0027225400 00000 n -0027232893 00000 n -0027234884 00000 n -0027241893 00000 n -0027244703 00000 n -0027540438 00000 n -0027544873 00000 n -0027831104 00000 n -0027835539 00000 n -0028948379 00000 n -0028952969 00000 n -0030518159 00000 n -0030521020 00000 n -0031063796 00000 n -0031067628 00000 n -0034388566 00000 n -0034437339 00000 n -0037541955 00000 n -0037590728 00000 n -0040656062 00000 n -0040704835 00000 n -0045445105 00000 n -0045493878 00000 n -0045775713 00000 n -0045778337 00000 n -0050597414 00000 n -0050646187 00000 n -0055158960 00000 n +0000339574 00000 n +0000339805 00000 n +0000340002 00000 n +0000340200 00000 n +0000341907 00000 n +0000342170 00000 n +0000342368 00000 n +0000342567 00000 n +0000342767 00000 n +0000342965 00000 n +0000343164 00000 n +0000343364 00000 n +0000345580 00000 n +0000345827 00000 n +0000346025 00000 n +0000346224 00000 n +0000346424 00000 n +0000346628 00000 n +0000347507 00000 n +0000347738 00000 n +0000347943 00000 n +0000348149 00000 n +0000350737 00000 n +0000350940 00000 n +0000353414 00000 n +0000353661 00000 n +0000353855 00000 n +0000354050 00000 n +0000354246 00000 n +0000354440 00000 n +0000355419 00000 n +0000355682 00000 n +0000355877 00000 n +0000356073 00000 n +0000356267 00000 n +0000356462 00000 n +0000356658 00000 n +0000356852 00000 n +0000357722 00000 n +0000357985 00000 n +0000358180 00000 n +0000358376 00000 n +0000358570 00000 n +0000358765 00000 n +0000358961 00000 n +0000359153 00000 n +0000361600 00000 n +0000361855 00000 n +0000362048 00000 n +0000362242 00000 n +0000362427 00000 n +0000362613 00000 n +0000362800 00000 n +0000364850 00000 n +0000365121 00000 n +0000365309 00000 n +0000365498 00000 n +0000365688 00000 n +0000365876 00000 n +0000366065 00000 n +0000366255 00000 n +0000366449 00000 n +0000367481 00000 n +0000367736 00000 n +0000367931 00000 n +0000368127 00000 n +0000368316 00000 n +0000368506 00000 n +0000368697 00000 n +0000370675 00000 n +0000370922 00000 n +0000371116 00000 n +0000371311 00000 n +0000371507 00000 n +0000371701 00000 n +0000373221 00000 n +0000373476 00000 n +0000373671 00000 n +0000373867 00000 n +0000374061 00000 n +0000374256 00000 n +0000374452 00000 n +0000377024 00000 n +0000377247 00000 n +0000377441 00000 n +0000378844 00000 n +0000379075 00000 n +0000379270 00000 n +0000379466 00000 n +0000382062 00000 n +0000382265 00000 n +0000384201 00000 n +0000384496 00000 n +0000384690 00000 n +0000384885 00000 n +0000385081 00000 n +0000385273 00000 n +0000385466 00000 n +0000385660 00000 n +0000385854 00000 n +0000386049 00000 n +0000386245 00000 n +0000386440 00000 n +0000387295 00000 n +0000387534 00000 n +0000387730 00000 n +0000387927 00000 n +0000388119 00000 n +0000389445 00000 n +0000389676 00000 n +0000389869 00000 n +0000390063 00000 n +0000393293 00000 n +0000393496 00000 n +0000396648 00000 n +0000396851 00000 n +0000398567 00000 n +0000398886 00000 n +0000399069 00000 n +0000399253 00000 n +0000399438 00000 n +0000399621 00000 n +0000399805 00000 n +0000399990 00000 n +0000400173 00000 n +0000400357 00000 n +0000400542 00000 n +0000400730 00000 n +0000400919 00000 n +0000401109 00000 n +0000401296 00000 n +0000402495 00000 n +0000402806 00000 n +0000402995 00000 n +0000403185 00000 n +0000403380 00000 n +0000403576 00000 n +0000403773 00000 n +0000403960 00000 n +0000404148 00000 n +0000404337 00000 n +0000404526 00000 n +0000404716 00000 n +0000404907 00000 n +0000405092 00000 n +0000406927 00000 n +0000407206 00000 n +0000407392 00000 n +0000407579 00000 n +0000407767 00000 n +0000407956 00000 n +0000408146 00000 n +0000408339 00000 n +0000408533 00000 n +0000408728 00000 n +0000411578 00000 n +0000411781 00000 n +0000414078 00000 n +0000414349 00000 n +0000414535 00000 n +0000414721 00000 n +0000414909 00000 n +0000415094 00000 n +0000415279 00000 n +0000415466 00000 n +0000415657 00000 n +0000417239 00000 n +0000417478 00000 n +0000417670 00000 n +0000417863 00000 n +0000418052 00000 n +0000419178 00000 n +0000419417 00000 n +0000419607 00000 n +0000419798 00000 n +0000419986 00000 n +0000421392 00000 n +0000421623 00000 n +0000421812 00000 n +0000422002 00000 n +0000424283 00000 n +0000424522 00000 n +0000424717 00000 n +0000424912 00000 n +0000425108 00000 n +0000426784 00000 n +0000427079 00000 n +0000427278 00000 n +0000427477 00000 n +0000427678 00000 n +0000427868 00000 n +0000428059 00000 n +0000428251 00000 n +0000428440 00000 n +0000428630 00000 n +0000428821 00000 n +0000429009 00000 n +0000431062 00000 n +0000431293 00000 n +0000431483 00000 n +0000431674 00000 n +0000435086 00000 n +0000435289 00000 n +0000438460 00000 n +0000438663 00000 n +0000440876 00000 n +0000441099 00000 n +0000441283 00000 n +0000441724 00000 n +0000441947 00000 n +0000442131 00000 n +0000442817 00000 n +0000443048 00000 n +0000443234 00000 n +0000443418 00000 n +0000443857 00000 n +0000444080 00000 n +0000444264 00000 n +0000447936 00000 n +0000448159 00000 n +0000448345 00000 n +0000452325 00000 n +0000452528 00000 n +0000456062 00000 n +0000456265 00000 n +0000459700 00000 n +0000459923 00000 n +0000460108 00000 n +0000461679 00000 n +0000461942 00000 n +0000462130 00000 n +0000462319 00000 n +0000462509 00000 n +0000462700 00000 n +0000462892 00000 n +0000463082 00000 n +0000465341 00000 n +0000465620 00000 n +0000465811 00000 n +0000466003 00000 n +0000466199 00000 n +0000466396 00000 n +0000466594 00000 n +0000466790 00000 n +0000466987 00000 n +0000467185 00000 n +0000470238 00000 n +0000470461 00000 n +0000470655 00000 n +0000472682 00000 n +0000472921 00000 n +0000473116 00000 n +0000473312 00000 n +0000473501 00000 n +0000474032 00000 n +0000474271 00000 n +0000474461 00000 n +0000474652 00000 n +0000474840 00000 n +0000476047 00000 n +0000476278 00000 n +0000476467 00000 n +0000476657 00000 n +0000480046 00000 n +0000480249 00000 n +0000484116 00000 n +0000484319 00000 n +0000488388 00000 n +0000488591 00000 n +0000489894 00000 n +0000490141 00000 n +0000490334 00000 n +0000490528 00000 n +0000490723 00000 n +0000490917 00000 n +0000492117 00000 n +0000492356 00000 n +0000492551 00000 n +0000492747 00000 n +0000492941 00000 n +0000493494 00000 n +0000493725 00000 n +0000493920 00000 n +0000494116 00000 n +0000496342 00000 n +0000496581 00000 n +0000496774 00000 n +0000496968 00000 n +0000497163 00000 n +0000499792 00000 n +0000500031 00000 n +0000500220 00000 n +0000500410 00000 n +0000500601 00000 n +0000501853 00000 n +0000502076 00000 n +0000502260 00000 n +0000502715 00000 n +0000502938 00000 n +0000503122 00000 n +0000503891 00000 n +0000504122 00000 n +0000504308 00000 n +0000504492 00000 n +0000504948 00000 n +0000505171 00000 n +0000505355 00000 n +0000505984 00000 n +0000506215 00000 n +0000506401 00000 n +0000506585 00000 n +0000507042 00000 n +0000507265 00000 n +0000507449 00000 n +0000508187 00000 n +0000508418 00000 n +0000508604 00000 n +0000508788 00000 n +0000509245 00000 n +0000509468 00000 n +0000509652 00000 n +0000510379 00000 n +0000510610 00000 n +0000510796 00000 n +0000510980 00000 n +0000511438 00000 n +0000511661 00000 n +0000511845 00000 n +0000513918 00000 n +0000514165 00000 n +0000514351 00000 n +0000514540 00000 n +0000514730 00000 n +0000514921 00000 n +0000517370 00000 n +0000517593 00000 n +0000517777 00000 n +0000518230 00000 n +0000518453 00000 n +0000518637 00000 n +0000519208 00000 n +0000519439 00000 n +0000519625 00000 n +0000519809 00000 n +0000520258 00000 n +0000520481 00000 n +0000520665 00000 n +0000521124 00000 n +0000521347 00000 n +0000521533 00000 n +0000524138 00000 n +0000524341 00000 n +0000527220 00000 n +0000527423 00000 n +0000528487 00000 n +0000528690 00000 n +0000530390 00000 n +0000530593 00000 n +0000532442 00000 n +0000532645 00000 n +0000535825 00000 n +0000536028 00000 n +0000538781 00000 n +0000538984 00000 n +0000541737 00000 n +0000541940 00000 n +0000542713 00000 n +0000542916 00000 n +0000544220 00000 n +0000544423 00000 n +0000547794 00000 n +0000547997 00000 n +0000551017 00000 n +0000551220 00000 n +0000554242 00000 n +0000554445 00000 n +0000555833 00000 n +0000556036 00000 n +0000558779 00000 n +0000558982 00000 n +0000559819 00000 n +0000560022 00000 n +0000561718 00000 n +0000561921 00000 n +0000563653 00000 n +0000563856 00000 n +0000565270 00000 n +0000565473 00000 n +0000568350 00000 n +0000568553 00000 n +0000571569 00000 n +0000571772 00000 n +0000573696 00000 n +0000573899 00000 n +0000576831 00000 n +0000577034 00000 n +0000579539 00000 n +0000579742 00000 n +0000581384 00000 n +0000581587 00000 n +0000584458 00000 n +0000584661 00000 n +0000585494 00000 n +0000585697 00000 n +0000588093 00000 n +0000588296 00000 n +0000591675 00000 n +0000591878 00000 n +0000593956 00000 n +0000594159 00000 n +0000597276 00000 n +0000597479 00000 n +0000600021 00000 n +0000600224 00000 n +0000602657 00000 n +0000602860 00000 n +0000605533 00000 n +0000605736 00000 n +0000608236 00000 n +0000608439 00000 n +0000610462 00000 n +0000610665 00000 n +0000613083 00000 n +0000613286 00000 n +0000615470 00000 n +0000615673 00000 n +0000617930 00000 n +0000618133 00000 n +0000620615 00000 n +0000620818 00000 n +0000622452 00000 n +0000622655 00000 n +0000625783 00000 n +0000625986 00000 n +0000626129 00000 n +0000626322 00000 n +0000626485 00000 n +0000626655 00000 n +0000626801 00000 n +0000626946 00000 n +0000627129 00000 n +0000627259 00000 n +0000627463 00000 n +0000627618 00000 n +0000627852 00000 n +0000628152 00000 n +0000628418 00000 n +0000628604 00000 n +0000628753 00000 n +0000629033 00000 n +0000629196 00000 n +0000629422 00000 n +0000629599 00000 n +0000629772 00000 n +0000629949 00000 n +0000630110 00000 n +0000630257 00000 n +0000630420 00000 n +0000630722 00000 n +0000630881 00000 n +0000631181 00000 n +0000631349 00000 n +0000631505 00000 n +0000631654 00000 n +0000631791 00000 n +0000631931 00000 n +0000632211 00000 n +0000632368 00000 n +0000632553 00000 n +0000632820 00000 n +0000632953 00000 n +0000633108 00000 n +0000633336 00000 n +0000633564 00000 n +0000633693 00000 n +0000633864 00000 n +0000634089 00000 n +0000634245 00000 n +0000634370 00000 n +0000634513 00000 n +0000634653 00000 n +0000634791 00000 n +0000634945 00000 n +0000635211 00000 n +0000635454 00000 n +0000635701 00000 n +0000635893 00000 n +0000636036 00000 n +0000636323 00000 n +0000636488 00000 n +0000636645 00000 n +0000636888 00000 n +0000637023 00000 n +0000637210 00000 n +0000637343 00000 n +0000637489 00000 n +0000637689 00000 n +0000637846 00000 n +0000638075 00000 n +0000638261 00000 n +0000638407 00000 n +0000638565 00000 n +0000638724 00000 n +0000638877 00000 n +0000639030 00000 n +0000639186 00000 n +0000639339 00000 n +0000639500 00000 n +0000639648 00000 n +0000639808 00000 n +0000639968 00000 n +0000640122 00000 n +0000640282 00000 n +0000640445 00000 n +0000640602 00000 n +0000640759 00000 n +0000640928 00000 n +0000641088 00000 n +0000641245 00000 n +0000641387 00000 n +0000641547 00000 n +0000641692 00000 n +0000641858 00000 n +0000642021 00000 n +0000642176 00000 n +0000642326 00000 n +0000642475 00000 n +0000642628 00000 n +0000642790 00000 n +0000642951 00000 n +0000643100 00000 n +0000643250 00000 n +0000643398 00000 n +0000643557 00000 n +0000643717 00000 n +0000643878 00000 n +0000644030 00000 n +0000644182 00000 n +0000644328 00000 n +0000644393 00000 n +0000648750 00000 n +0000657207 00000 n +0000661656 00000 n +0000665117 00000 n +0000671213 00000 n +0000672001 00000 n +0000673234 00000 n +0000673466 00000 n +0000674064 00000 n +0000674218 00000 n +0000676136 00000 n +0000676384 00000 n +0000677236 00000 n +0000677399 00000 n +0000678604 00000 n +0000678836 00000 n +0000679375 00000 n +0000679530 00000 n +0000680496 00000 n +0000680724 00000 n +0000681190 00000 n +0000681339 00000 n +0000683020 00000 n +0000683266 00000 n +0000683888 00000 n +0000684047 00000 n +0000684438 00000 n +0000684678 00000 n +0000684913 00000 n +0000685073 00000 n +0000685199 00000 n +0004389926 00000 n +0004460279 00000 n +0005242590 00000 n +0005247044 00000 n +0005974964 00000 n +0005979410 00000 n +0006267656 00000 n +0006272110 00000 n +0006397963 00000 n +0006430398 00000 n +0006434578 00000 n +0006436255 00000 n +0006446679 00000 n +0006448589 00000 n +0006940758 00000 n +0006943220 00000 n +0007116484 00000 n +0007163014 00000 n +0007785550 00000 n +0007788254 00000 n +0007817481 00000 n +0007817842 00000 n +0007818689 00000 n +0007818947 00000 n +0007822116 00000 n +0007822412 00000 n +0007825175 00000 n +0007825449 00000 n +0007833774 00000 n +0007834118 00000 n +0007836190 00000 n +0007837073 00000 n +0007838739 00000 n +0007839493 00000 n +0007840054 00000 n +0007840740 00000 n +0007841301 00000 n +0007841990 00000 n +0007842551 00000 n +0007843305 00000 n +0007845757 00000 n +0007846057 00000 n +0007848508 00000 n +0007848808 00000 n +0007849345 00000 n +0007849645 00000 n +0007852729 00000 n +0007853205 00000 n +0007854157 00000 n +0007854575 00000 n +0007857340 00000 n +0007857616 00000 n +0007868123 00000 n +0007871272 00000 n +0007871772 00000 n +0007872048 00000 n +0007875842 00000 n +0007876249 00000 n +0007877057 00000 n +0007877400 00000 n +0007878325 00000 n +0007878668 00000 n +0008390877 00000 n +0008417783 00000 n +0008426130 00000 n +0008426474 00000 n +0008440689 00000 n +0008441054 00000 n +0008453515 00000 n +0008453859 00000 n +0008462397 00000 n +0008462746 00000 n +0008470960 00000 n +0008471320 00000 n +0008472902 00000 n +0008473157 00000 n +0008473698 00000 n +0008474072 00000 n +0008474617 00000 n +0008474991 00000 n +0008475537 00000 n +0008475911 00000 n +0008476456 00000 n +0008476830 00000 n +0008477377 00000 n +0008477751 00000 n +0008479270 00000 n +0008479525 00000 n +0008480071 00000 n +0008480445 00000 n +0008481744 00000 n +0008481999 00000 n +0008482556 00000 n +0008482811 00000 n +0008486702 00000 n +0008487120 00000 n +0008487770 00000 n +0008488068 00000 n +0009266366 00000 n +0009268735 00000 n +0009354523 00000 n +0009355922 00000 n +0009405986 00000 n +0009680412 00000 n +0009684030 00000 n +0009802143 00000 n +0009806023 00000 n +0011000730 00000 n +0012767403 00000 n +0012778839 00000 n +0012779246 00000 n +0012785516 00000 n +0012785908 00000 n +0012790861 00000 n +0012791232 00000 n +0012941467 00000 n +0012943070 00000 n +0013057728 00000 n +0013059681 00000 n +0013181031 00000 n +0013183163 00000 n +0013187216 00000 n +0013188017 00000 n +0013194000 00000 n +0013194761 00000 n +0013314078 00000 n +0013316213 00000 n +0013424876 00000 n +0013427024 00000 n +0013531367 00000 n +0013533325 00000 n +0013638095 00000 n +0013640243 00000 n +0013760193 00000 n +0013762344 00000 n +0013871473 00000 n +0013873694 00000 n +0013984083 00000 n +0013986054 00000 n +0014067475 00000 n +0014068174 00000 n +0014086097 00000 n +0014086709 00000 n +0014626430 00000 n +0014629539 00000 n +0015145079 00000 n +0015148015 00000 n +0015681239 00000 n +0015684107 00000 n +0016223868 00000 n +0016226815 00000 n +0016766356 00000 n +0016769322 00000 n +0016788958 00000 n +0016789895 00000 n +0016817696 00000 n +0016818095 00000 n +0016819642 00000 n +0016819925 00000 n +0016821322 00000 n +0016821605 00000 n +0017078951 00000 n +0017081335 00000 n +0017102700 00000 n +0017103621 00000 n +0017610663 00000 n +0017613610 00000 n +0018153136 00000 n +0018155855 00000 n +0018700302 00000 n +0018703309 00000 n +0018733992 00000 n +0018735904 00000 n +0018761155 00000 n +0018762039 00000 n +0018776851 00000 n +0018777212 00000 n +0018793683 00000 n +0018794022 00000 n +0018812607 00000 n +0018813738 00000 n +0019135101 00000 n +0019137732 00000 n +0019138214 00000 n +0019138488 00000 n +0019138984 00000 n +0019139258 00000 n +0019139788 00000 n +0019140062 00000 n +0019141410 00000 n +0019141982 00000 n +0019143360 00000 n +0019143948 00000 n +0019146772 00000 n +0019147048 00000 n +0019149518 00000 n +0019149794 00000 n +0019150347 00000 n +0019150623 00000 n +0019160091 00000 n +0019160836 00000 n +0019162445 00000 n +0019163034 00000 n +0019163982 00000 n +0019164325 00000 n +0019166356 00000 n +0019166628 00000 n +0019168641 00000 n +0019168913 00000 n +0019178728 00000 n +0019181672 00000 n +0019222677 00000 n +0019223720 00000 n +0019225412 00000 n +0019226623 00000 n +0019235067 00000 n +0019236203 00000 n +0019236456 00000 n +0019237540 00000 n +0019237793 00000 n +0019238930 00000 n +0019239393 00000 n +0019240944 00000 n +0019241407 00000 n +0019243149 00000 n +0019243612 00000 n +0022808916 00000 n +0022857689 00000 n +0026619416 00000 n +0026668189 00000 n +0027151895 00000 n +0027156337 00000 n +0027157082 00000 n +0027157380 00000 n +0027158092 00000 n +0027158390 00000 n +0027159073 00000 n +0027159371 00000 n +0027160029 00000 n +0027160327 00000 n +0027229039 00000 n +0027232983 00000 n +0027240476 00000 n +0027242467 00000 n +0027249476 00000 n +0027252286 00000 n +0027548021 00000 n +0027552456 00000 n +0027838687 00000 n +0027843122 00000 n +0028955962 00000 n +0028960552 00000 n +0030525742 00000 n +0030528603 00000 n +0031071379 00000 n +0031075211 00000 n +0034396149 00000 n +0034444922 00000 n +0037549538 00000 n +0037598311 00000 n +0040663645 00000 n +0040712418 00000 n +0045452688 00000 n +0045501461 00000 n +0045783296 00000 n +0045785920 00000 n +0050604997 00000 n +0050653770 00000 n +0055166543 00000 n trailer << -/Size 1355 +/Size 1363 /Root 3 0 R /Info 2 0 R >> startxref -55229313 +55236896 %%EOF diff --git a/site/search/search_index.json b/site/search/search_index.json index ff9f392..a9075c0 100644 --- a/site/search/search_index.json +++ b/site/search/search_index.json @@ -1 +1 @@ -{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"index.html","text":"Rapport Track Trends V1.0 Rohmer Maxime Travail de dipl\u00f4me Technicien ES 2023 Introduction R\u00e9sum\u00e9 Track Trends est un outil de r\u00e9cup\u00e9ration et d'analyse de donn\u00e9es de courses de Formule 1. Pour le contexte, en dehors des cours, j'exerce diff\u00e9rentes activit\u00e9s dont celle de Live Ticker F1 pour le 20 minutes. Pour m'aider dans ce travail, j'utilise actuellement la F1TV \u00e0 laquelle je suis abonn\u00e9 qui me propose non seulement un feed vid\u00e9o de meilleure qualit\u00e9 avec des commentaires plus pertinents que ceux de la RTS mais qui aussi me permet d'acc\u00e9der \u00e0 un feed vid\u00e9o tr\u00e8s important : la chaine data. Ce dernier ressemble \u00e0 cela : \"Screenshot du feed data de la f1tv\" (Attention ce n'est pas un joli tableau HTML, mais bien une vid\u00e9o qui contient un tableau.) Sauf que toutes les informations sont \u00e9tal\u00e9es p\u00eale-m\u00eale sans hi\u00e9rarchie ce qui fait que cela me prendrait trop de temps de tout d\u00e9chiffrer \u00e0 chaque fois, ce qui me fait rater des choses int\u00e9ressantes. Le but du projet est donc de fournir un outil qui hi\u00e9rarchise et affiche diff\u00e9remment les donn\u00e9es pour faciliter leur lecture et me permettre de faire de meilleurs commentaires. Abstract Track Trends is a Formula 1 data and analysis tool. To understand everything, a little bit of context. In my free time I have multiple activities and one is to be the Live Ticker F1 for the local journal \"20 minutes\". to help me in this work I'm currently using the F1TV to which I'm currently subscribed because it provides me with a better video feed with better commentary than the ones from the RTS (in my opinion) but also because it gives me access to a very important video feed : the data channel See the screenshot above to see what it looks like. [note: It's a pretty HTML table but a full on video feed that contains a table (probably, so you can't access data directly)] You can see a lot of data all well and good BUT! All the data is displayed the same in a big table which make it really hard to read totally in a hurry, which means that I miss a lot of useful information. The point of the project then is to provide with a tool that can display those data by taking into account their relevance. That would help me not miss any and provide a better commentary by never missing out battles, and be able to better write with the time I saved by using it. Description du besoin Comme expliqu\u00e9 dans le r\u00e9sum\u00e9, je suis Live Ticker F1. Mais pour mieux comprendre le besoin que j'ai, je pense qu'il est pertinent de comprendre comment je travaille. Pendant un Grand Prix de Formule 1 j'ai plusieurs t\u00e2ches \u00e0 effectuer : Suivre les diff\u00e9rents \u00e9v\u00e8nements du Grand Prix Changer le titre et la photo de titre du Live Chercher des Tweets ou des Images \u00e0 int\u00e9grer Ecrire les commentaires en faisant attention \u00e0 dire ce qu'il se passe mais aussi l\u2019expliquer, ce que cela implique, mais aussi ce que cela veut dire pour le reste de la course. Comprendre et expliquer les strat\u00e9gies Tout cela toutes les cinq minutes max... Cela veut dire que je dois \u00eatre le plus rapide possible quand je cherche des informations. Et comme le tableau en comporte trop et bien, je suis oblig\u00e9 de le lire en diagonale. Par exemple dans le tableau, les infos que je cherche le plus sont : Le nombre de places gagn\u00e9es (surtout au d\u00e9part) Les \u00e9carts entre les pilotes (surtout ceux qui sont en dessous de deux secondes). Les pneus de chaque pilote et combien de tours, ils ont fait dessus Les temps d'arr\u00eats aux stands Les temps au tour (surtout pour la strat\u00e9gie) Mais pleins d'autres informations existent si on les recoupe sur plusieurs tours. Un outil qui permettrait de mettre en \u00e9vidence les informations importantes serait donc une tr\u00e8s grosse plus-value pour mon travail et s'il est facile \u00e0 installer et \u00e0 utiliser, il se pourrait qu'il devienne indispensable. Cahier des charges Il s'agit d'une version coup\u00e9e du cahier des charges qui ne contient pas l'explication du contexte. Mais l'original est disponible sur ce site \u00e9galement. Il est toutefois normal d'y voir des choses r\u00e9p\u00e9t\u00e9es ou l\u00e9g\u00e8rement diff\u00e9rentes, en effet, il n'a pas \u00e9t\u00e9 \u00e9crit en m\u00eame temps que le reste de ce document. Projet Un outil de style compagnon sous forme d'application C# Windows Form qui r\u00e9cup\u00e8re en temps r\u00e9el les informations de la course et affiche les informations les plus importantes. Le but est non seulement de faciliter mon job, mais aussi faire en sorte d'am\u00e9liorer la plus-value de mon travail en me permettant de fournir des commentaires qui ne sont pas disponibles pour le tout venant qui regarde simplement le flux RTS. Exemples : Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand Maintenant afficher diff\u00e9remment les infos, c'est sympa, mais cela serait encore mieux de traiter ces data et de permettre des petites pr\u00e9dictions. Exemples : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents R\u00e9alisation Malheureusement, la Formula 1 Management ne propose aucune API publique qui puisse nous permettre de faire ce projet \"simplement\". La raison la plus probable \u00e9tant qu'Amazon avec son service AWS propose exactement ce genre de services pour le flux t\u00e9l\u00e9vis\u00e9 et il doit y avoir un contrat d'exclusivit\u00e9. Il existe des API \"Pirates\" faites par la communaut\u00e9, le probl\u00e8me est qu'elles ne sont pas forc\u00e9ment des plus pratiques \u00e0 utiliser. Mais comme je poss\u00e8de un abonnement premium ++ \u00e0 la F1TV, j'ai acc\u00e8s pour chaque grand prix \u00e0 un flux vid\u00e9o nomm\u00e9 : DATA F1 CHANNEL Qui ressemble \u00e0 \u00e7a : \"Data channel exemple\" Donc la seule fa\u00e7on que je vois de r\u00e9cup\u00e9rer ces donn\u00e9es est de les prendre directement sur ce feed. M\u00eame si le but final de l'application est de faire pleins de choses super avec les datas, le gros du projet va surtout \u00eatre la r\u00e9cup\u00e9ration des donn\u00e9es et leur stockage. Les donn\u00e9es viennent du flux vid\u00e9o et ainsi dans un premier temps, il va falloir r\u00e9cup\u00e9rer d'une mani\u00e8re ou d'une autre des images qui viennent d'un grand prix en direct ou en rediffusion. Ensuite, dans un second temps, il faut lire les informations directement sur l'image en utilisant une librairie pr\u00e9vue pour (exemple Tesseract) et v\u00e9rifier l'int\u00e9grit\u00e9 de ces derni\u00e8res pour qu'on puisse ensuite les stocker. Dans un troisi\u00e8me temps, il faut stocker toutes ces donn\u00e9es dans une forme qui permette d'aller facilement faire des requ\u00eates de r\u00e9cup\u00e9ration et d\u00e9j\u00e0 pr\u00e9parer des m\u00e9thodes qui permettent de r\u00e9cup\u00e9rer des infos importantes (ex : la moyenne des cinq derniers tours, le temps moyen d'arr\u00eat etc.) pour faciliter la derni\u00e8re \u00e9tape Quand tout cela est fait, on peut ensuite s'amuser un peu avec les Data. La derni\u00e8re \u00e9tape est donc l'affichage. L'id\u00e9e est de cr\u00e9er une Windows Form qui contienne toutes ces informations dans un format beaucoup plus lisible et avec laquelle on pourrait interagir pour permettre de plus facilement commenter les Grands Prix. (exemple plus bas avec un croquis de ce \u00e0 quoi l'application pourrait ressembler) Voici la liste des donn\u00e9es qui pourraient \u00eatre affich\u00e9es (Non contractuel, simplement des id\u00e9es). Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand La moyenne de temps que les pilotes perdent dans les stands La performance moyenne des 5 types de pneus La moyenne de temps de chaque pilote sur le pneu actuel Le nombre de points que chaque pilote gagnerait selon sa position Le classement de la course Voire m\u00eame si c'est possible : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents Pr\u00e9dire les temps au tour de chaque pilote selon l'usure des pneus Voici un exemple d'interface possible pour une page : \"Proto\" Cas d'utilisation '*'On va consid\u00e9rer que tous les user ont un abonnement F1 TV PRO Un user veut r\u00e9cup\u00e9rer les data : Il ouvre son navigateur et lance la page DATA de la F1 TV Il calibre la capture des data via le programme (pour la premi\u00e8re utilisation). Il confirme que les donn\u00e9es initiales sont bonnes (pour la premi\u00e8re utilisation). Il regarde tranquille son Grand Prix Le programme r\u00e9cup\u00e8re les data : Il r\u00e9cup\u00e8re des images depuis la F1TV Il utilise Tesseract (ou autre) pour en r\u00e9cup\u00e9rer les infos. Il met ces infos dans un Objet Pilote, dans un Objet course avec un attribut tour pour hi\u00e9rarchiser les data Pour ce qui est de l'affichage, l'id\u00e9e est de faire une application C# comme on l'a appris \u00e0 l'\u00e9cole, mais avec assez de style pour qu'elle puisse \u00eatre agr\u00e9able \u00e0 utiliser. Quand le programme affiche les data : Il prend les donn\u00e9es venant directement de la F1TV. Il affiche diff\u00e9remment les donn\u00e9es pour permettre une meilleure lisibilit\u00e9 Il interpr\u00e8te avec des r\u00e8gles plut\u00f4t simples certaines data pour faire des minipr\u00e9dictions ou aider \u00e0 la lecture Il r\u00e9cup\u00e8re des infos d'autres courses pour les comparer et proposer des pr\u00e9dictions plus int\u00e9ressantes Difficult\u00e9s techniques R\u00e9cup\u00e9rer un flux vid\u00e9o plut\u00f4t propre malgr\u00e9 les contres mesures de la F1 TV pour en emp\u00eacher la lecture par un logiciel Si on doit passer par une capture d'\u00e9cran, trouver un moyen de stocker les donn\u00e9es de mani\u00e8re \u00e0 pr\u00e9voir que parfois un tour pourrait avoir plus de donn\u00e9es qu'un autre, que le user peut mettre pause, ou m\u00eame qu\u2019il revienne en arri\u00e8re. D\u00e9velopper des algorithmes pour r\u00e9cup\u00e9rer les donn\u00e9es comme les diff\u00e9rents pneus utilis\u00e9s ou l'activation du DRS ainsi que d\u00e9velopper des moyens de nettoyer les r\u00e9sultats de l'OCR (Par exemple utiliser diff\u00e9rents champs redondants pour comparer les r\u00e9sultats) Stocker les donn\u00e9es sur une base pour les traiter plus tard tout en pr\u00e9voyant un moyen de voir les stats live D\u00e9velopper des algorithmes de pr\u00e9diction qui prennent en compte d'anciennes courses pour tenter de pr\u00e9dire des choses comme les arr\u00eats aux stands par exemple. Diff\u00e9rences sur le cahier des charges [\u00c0 remplir dans les derni\u00e8res semaines du travail de dipl\u00f4me] Planning pr\u00e9visionnel Mes suiveurs m'ont demand\u00e9 un planning de type GANTT pour ce travail de dipl\u00f4me Je n'ai pas utilis\u00e9 un logiciel particulier pour faire ce dernier, mais je me suis inspir\u00e9 des principes fondamentaux d'un diagramme de ce type. Comme l'original a \u00e9t\u00e9 fait sur Excel, je ne peux pas vraiment l'ins\u00e9rer de mani\u00e8re lisible ici, mais il est disponible dans le dossier Planning. Mais voici un r\u00e9sum\u00e9 de son contenu : T\u00e2ches J'ai d\u00e9cid\u00e9 de d\u00e9composer mon planning en trois grands types de t\u00e2ches. Programmation Documentation Tests L'id\u00e9e est de permettre une meilleure lisibilit\u00e9 et de me permettre \u00e0 moi de me faire plus facilement \u00e0 l'id\u00e9e de ce qu'il m'attend. Voici la liste des t\u00e2ches par rubrique : PT Cette rubrique contient les t\u00e2ches qui n'ont pas leur place dans les trois cat\u00e9gories principales. PT1 / pr\u00e9paration au travail de dipl\u00f4me (2) Cette t\u00e2che est un peu hors cat\u00e9gorie, mais c'est normal, c'est une supert\u00e2che qui regroupe beaucoup de choses. C'est une t\u00e2che qui est planifi\u00e9e pour deux jours et qui normalement devrait \u00eatre faite les deux premiers jours du travail. Le but est de pr\u00e9parer tout ce qui peut \u00eatre pr\u00e9par\u00e9 en avance niveau documentation et mise en place pour ne pas avoir besoin de s'en soucier ensuite. DT Rubrique documentation qui contient toutes les t\u00e2ches en rapport de pr\u00e8s ou de loin avec la documentation du projet. DT1 Cr\u00e9ation du poster (1) Cette t\u00e2che consiste \u00e0 faire une version num\u00e9rique du poster qui soit en accord avec les consignes qu'on nous a donn\u00e9es. Le but est aussi et surtout de faire poster dont je sois fier et que je sois content de montrer. Il y a d\u00e9j\u00e0 des croquis de poster et j'ai clairement pr\u00e9vu de travailler sur \u00e7a pendant les vacances alors, je n'ai mis qu'un jour et je l'ai plac\u00e9 juste avant le rendu de ce dernier. DT2 Documentation Analyse de l'existant (2) Cette t\u00e2che est d\u00e9di\u00e9e \u00e0 l'\u00e9criture de la documentation et plus pr\u00e9cis\u00e9ment de l'analyse de l'existant. Comme il y a pas mal de technologies utilis\u00e9es dans mon projet, j'aimerais faire correctement un vrai debrief de pourquoi j'ai utilis\u00e9 l'une ou l'autre alors, j'ai assign\u00e9 deux jours dessus. DT3 Documentation Analyse organique (5) Cette t\u00e2che est la plus grosse dans la cat\u00e9gorie documentation. Il s'agit de documenter comment l'application fonctionne. J'y ai mis cinq jours et je pense que c'est un minimum car c'est dans cette t\u00e2che que je vais devoir d\u00e9tailler exactement comment fonctionne chaque partie du projet. Ces cinq jours sont \u00e9parpill\u00e9s sur le projet en g\u00e9n\u00e9ral \u00e0 la fin du d\u00e9veloppement de chaque grande partie de projet. Le but est de ne rien oublier et de ne pas avoir \u00e0 tout faire en m\u00eame temps. DT4 Documentation Analyse fonctionnelle (2) Cette t\u00e2che est d\u00e9j\u00e0 moins grosse, elle consiste \u00e0 documenter le fonctionnement de l'application et comment utiliser les composants que j'ai d\u00e9velopp\u00e9s. Je l'ai mis en fin de projet, car comme j'ai l'habitude de faire des analyses fonctionnelles plut\u00f4t pr\u00e9cises, le moindre changement dans l'UI peut tout rendre obsol\u00e8te. J'y ai mis deux jours, car j'aimerais correctement documenter avec de bonnes photos et sc\u00e9narios pour qu'on puisse voir toutes les possibilit\u00e9s de l'application. DT5 Documentation Tests (1) Cette t\u00e2che est un peu plus petite qu'elle ne le devrait. Elle concerne la documentation des diff\u00e9rents tests. Je n'y ai mis qu'un seul jour, car en r\u00e9alit\u00e9 les diff\u00e9rentes t\u00e2ches de tests contiennent aussi beaucoup de documentation, DT6 Documentation Reste (2) Cette t\u00e2che est une t\u00e2che un peu vague. Elle contient toutes les actions autres que j'aurai besoin de faire (Mise au propre, orthographe, g\u00e9n\u00e9ration de PDF ...). J'y ai mis deux jours pour avoir un peu de marge, car ce sont toujours des t\u00e2ches qui paraissent faciles, mais qui \u00e0 la fin prennent beaucoup de temps si on les fait bien. PT Rubrique programmation qui contient toutes les t\u00e2ches qui touchent \u00e0 la programmation et au d\u00e9veloppement de l'application. PT1 Programmation r\u00e9cup\u00e9ration des images (3) Cette t\u00e2che est estim\u00e9e \u00e0 seulement trois jours, il ne faut pas s'y m\u00e9prendre, c'est une des t\u00e2ches les plus dures et lourdes niveaux documentation en explications. Cependant, un POC (Proof Of Concept) assez avanc\u00e9 a d\u00e9j\u00e0 \u00e9t\u00e9 fait et donc cela permet de n'envisager que trois jours, car il suffit de l'impl\u00e9menter et de la paufinner. Cette t\u00e2che consiste \u00e0 prendre en entr\u00e9e un lien de Grand Prix et de sortir une image tous les x secondes de la page DATA. Cela peut sembler simple, mais pour le faire sans prendre d'espace d'\u00e9cran et ne demandant pas \u00e0 l'utilisateur de copier-coller quoi que ce soit o\u00f9 de donner ses identifiants F1TV c'est un challenge. Cela peut paraitre curieux alors de mettre cette t\u00e2che loin dans le planning m\u00eame si c'est la premi\u00e8re \u00e9tape du projet. Encore une fois cela s'explique avec le fait qu'il y a d\u00e9j\u00e0 un POC qui fonctionne \u00e0 peu pr\u00e8s et que donc pr\u00e9f\u00e8re commencer avec des t\u00e2ches plus incertaines dans le cas o\u00f9 elles prendraient plus de temps que pr\u00e9vu. PT2 Programmation OCR (5) Cette t\u00e2che consiste \u00e0 d\u00e9velopper la partie qui reconnait le texte sur les images. C'est tr\u00e8s certainement la t\u00e2che qui risque le plus de d\u00e9border car c'est celle qui est la plus complexe techniquement puisqu'elle demande non seulement la lecture sur image, mais aussi le d\u00e9veloppement d'algorithmes de traitement de cette donn\u00e9e pour \u00eatre s\u00fbr qu'elle a bien \u00e9t\u00e9 lue. J'y ai ainsi allou\u00e9 cinq jours, mais j'esp\u00e8re que j'arriverai \u00e0 gagner du temps sur les autres pour y allouer plus dans le planning effectif, car je suis convaincu que plus, on y passe du temps, meilleur sera le r\u00e9sultat. PT3 Programmation, stockage et mod\u00e8le (5) Cette partie est moins technique, mais concerne le stockage des donn\u00e9es que nous retourne la lecture des images. Mais elle va demander de la r\u00e9flexion et de l'intelligence de programmation, car il faut que cette partie anticipe les besoins de la vue et pr\u00e9pare un terrain fertile qui ne demande pas un refactor quand on passera au d\u00e9veloppement de la vue. C'est pour cela que je lui ai aussi assign\u00e9 cinq jours de travail et elle doit absolument \u00eatre commenc\u00e9e apr\u00e8s la lecture. PT4 Programmation Vue de l'APP (5) Cette partie peut \u00eatre horrible comme tr\u00e8s facile, cela d\u00e9pend compl\u00e8tement de la qualit\u00e9 du travail avant. Si le mod\u00e8le est parfait et que les donn\u00e9es sont int\u00e8gres, cela devrait \u00eatre plut\u00f4t simple de les afficher de mani\u00e8re int\u00e9ressante. Cependant, cette partie d\u00e9bordera s\u00fbrement un peu, car tout le temps gagn\u00e9 avec de bonnes donn\u00e9es sera utilis\u00e9 pour tenter de faire de la pr\u00e9diction. Pour ces raisons, je lui ai assign\u00e9 \u00e9galement cinq jours de travail et elle doit absolument \u00eatre faite apr\u00e8s le mod\u00e8le. PT5 Programmation mise en commun (3) Cette t\u00e2che est aussi un petit peu sp\u00e9ciale, car elle regroupe plusieurs choses. En gros, chaque partie de programmation sera s\u00fbrement assez ind\u00e9pendante et il faudra \u00e0 un moment faire un seul projet C# qui contient tout. Il est difficile d'estimer \u00e0 quel point cela va \u00eatre compliqu\u00e9 alors, j'ai \u00e9t\u00e9 conservateur et j'ai mis trois jours. TT Cette rubrique contient les t\u00e2ches qui sont uniquement des tests. La plupart des t\u00e2ches de programmations contiennent d\u00e9j\u00e0 des tests, mais certaines demandent une attention particuli\u00e8re. TT1 Tests OCR (2) Cette t\u00e2che est une des t\u00e2ches les plus importantes. Son but est de faire un protocole de tests complet qui permette de comparer les diff\u00e9rents algorithmes de reconnaissance de texte. Je l'ai mise apr\u00e8s la reconnaissance, mais m\u00eame maintenant en \u00e9crivant ces lignes, je me dis que dans le planning effectif, elle sera faite pendant la t\u00e2che de programmation. En effet, comment savoir si mon tout nouvel algorithme est r\u00e9ellement mieux que le pr\u00e9c\u00e9dent. Je pr\u00e9vois deux jours, car je pense que faire le dataset va prendre beaucoup de temps, il faut pr\u00e9voir un certain nombre d'images et de texte qui pourront ensuite \u00eatre donn\u00e9es sous forme de tests. C'est certes une t\u00e2che de test, mais c'est aussi de la programmation. TT2 Tests finaux (2) Cette t\u00e2che de tests est un peu vague, elle regroupe les diff\u00e9rents tests pour v\u00e9rifier que les donn\u00e9es sont bien affich\u00e9es correctement. Ce qui serait cool si j'ai du temps en fin de travail de dipl\u00f4me serait de faire un syst\u00e8me de test qui permet d'entrainer le programme \u00e0 mieux reconnaitre certaines choses comme des arr\u00eats aux stands si on lui donne les trois derni\u00e8res ann\u00e9es de grands Prix. J'ai mis une dur\u00e9e arbitraire de deux jours, mais je ne sais pas vraiment combien de temps cela va vraiment durer. Elle est \u00e9videmment \u00e0 effectuer une fois que tout est \u00e0 peu pr\u00e8s termin\u00e9. Planning effectif et diff\u00e9rences [A remplir dans les derni\u00e8res semaines du travail de dipl\u00f4me] Analyse fonctionnelle [A remplir au fur et \u00e0 mesure dans la seconde moiti\u00e9 du travail de dipl\u00f4me] Analyse Organique [A remplir au fur et \u00e0 mesure que les features sont d\u00e9velopp\u00e9es] R\u00e9cup\u00e9ration des images Voici la premi\u00e8re grande \u00e9tape du projet. Pour rappel, Amazon h\u00e9berge directement le site de la F1TV et poss\u00e8de les droits sur les donn\u00e9es de la F1. C'est sous le nom de AWS (le service d'h\u00e9bergement d'Amazon) que la firme apparait en tant que sponsor. On peut voir ce nom appara\u00eetre assez souvent quand on regarde un Grand Prix car comme ils ont la main-mise sur les donn\u00e9es ils peuvent ins\u00e8rer des bandeaux d'informations sur le flux public sur ce qu'il se passe voir m\u00eame faire des pr\u00e9dictions (Bien qu'un peu bancales) \"AWS example 1\" Ce service s'appelle F1 Insights (Oui c'est un meilleur nom de projet que F1 Companion mais bon) et c'est, je pense, la raison pour laquelle on ne voit aucune API publique qui permette de correctement se renseigner en don\u00e9es en direct pendant un Grand Prix. Ils ont du d\u00e9gotter un juteux contrat pour s'occuper de toute l'infrastructure digitale de la F1 (du moins publique) en \u00e9change d'une exclusivit\u00e9 totale sur certaines choses comme les Data \"AWS example 2\" Evidemment je ne fais que conjecturer et ce que j'ai dit n'est pas \u00e0 prendre au pied de la lettre mais c'est une explication possible je pense de pourquoi il est si difficile de trouver des donn\u00e9es sur la F1 facilement en temps r\u00e9el. Il existe bien quelques API un peu bancales publiques, mais le probl\u00e8me c'est qu'elles ne sont vraiment pas suffisante et je ne peux pas leur faire confiance quand je commente. Ce qu'il m'aurait fallut c'est une API publique et officielle qui me permette d'\u00eatre sur que les donn\u00e9es sont les bonnes et qu'elles arrivent le plus vite possible. On pourrait croire que c'est impossible car cela n'existe pas comme je l'ai dit MAIS ! Ce n'est pas compl\u00eatement vrai. En effet depuis que je poss\u00e8de un abonnement \u00e0 la F1TV, il existe une source d'informations tr\u00e8s pr\u00e9cieuse qui m'aide \u00e9norm\u00e9ment dans mon quotidien de commentateur de Formule 1. La \"DATA CHANNEL\". La Data Channel est une page de la F1TV qui permet, pour chaque Grand Prix, de visualiser, sous la forme d'un flux vid\u00e9o, diff\u00e9rentes informations capitales sur la course. \"Data channel example\" Le probl\u00e8me, c'est que comme je viens de le dire, ces donn\u00e9es ne sont pas accessibles comme un tableau HTML ou un flux RSS ou un tableau JSON. C'est un flux vid\u00e9o. Il faut savoir qu'entretenir une diffusion de flux vid\u00e9o en 1080P pendant deux heures accessible par des milliers d'abonn\u00e9s est EXTR\u00caMENT cher surtout quand on le compare \u00e0 simplement afficher les donn\u00e9es dans un tableau. Ce qui veut dire que ce choix est d\u00e9lib\u00e9r\u00e9 et a un sens au niveau \u00e9conomique. Je pense donc que c'est justement pour \u00e9viter que des petits malins puissent simplement venir scraper l'int\u00e9gralit\u00e9 des donn\u00e9es qu'ils proposent et fasse sa propre API. (C'est d'ailleurs un des sites avec la meilleure protection anti bot du monde) MAIS ce n'est pas par ce que les donn\u00e9es ne sont pas facile \u00e0 avoir qu'elles sont IMPOSSIBLE \u00e0 avoir. Et c'est la que ce projet entre en jeu. Mais pour d\u00e9coder les donn\u00e9es d'une image il faut dabord ... (roulement de tambours) ... Avoir des images ! Et c'est la que vient se glisser cette partie du projet. Comment faire ? Le but de ce segment est de se concentrer sur la r\u00e9cup\u00e8ration et la mise \u00e0 disposition pour le reste du programme, des images en direct de la F1TV dans la meilleure qualit\u00e9 possible et dans les meilleurs d\u00e9lais. Pour ce faire il y a plusieurs solutions : Reverse engeneer la F1TV pour acc\u00e8der directement au flux sans passer par la plateforme internet et pouvoir prendres images \u00e0 volont\u00e9. Avoir tout simplement une page de la F1TV ouverte sur un \u00e9cran et prendres des screenshots \u00e0 intervals r\u00e9guliers. Simuler un navigateur internet sans qu'il soit affich\u00e9 et le contr\u00f4ler automatiquement pour qu'il prenne des captures. La premi\u00e8re option aurait \u00e9t\u00e9 la plus \u00e9l\u00e9gante mais lors d'un POC que je tentais de r\u00e9aliser je me suis rendu compte que cela serait un peu trop compliqu\u00e9 et long \u00e0 faire. Sans compter le fait que les rediffusions de Grand Prix ne sont pas g\u00e8r\u00e9es de la m\u00eame mani\u00e8re que les diffusions en live. Et que pour faire des Tests en live il faudrait attendre \u00e0 chaque fois un weekend de Grand Prix et le faire en plus du commentaire que je dois produire. Pour toutes ces raisons et bien d'autres je l'ai rang\u00e9e dans la case \"Trop dur, Trop chiant, S\u00fbrement ill\u00e9gal\" (Oui je sais c'est une cat\u00e9gorie bien sp\u00e9cifique mais c'est ma documentation je fais ce que je veux) La troisi\u00e8me option aurait \u00e9t\u00e9 la plus simple (et moins dr\u00f4le) et je suis presque s\u00fbr que je peux impl\u00e9menter cette derni\u00e8re en moins d'une apr\u00e8s-midi. Sauf qu'elle apporte de gros soucis. On ne peux pas garantir l'int\u00e9grit\u00e9 et la continuit\u00e9 des donn\u00e9es si l'utilisateur avance ou fait pause m\u00eame par simple inadvertance. La moindre fen\u00eatre qui s'afficherait devant ruinerait toute la reconnaissance de caract\u00e8res. On ne peut pas contr\u00f4ler la qualit\u00e9 du flux et on est oblig\u00e9 de faire confiance en l'utilisateur On ne peut pas vraiment automatiser quoi que ce soit niveau tests ou m\u00eame pour faire du scrapping auto pour remplir une base de donn\u00e9e. Et finalement le pire inconv\u00e9nient : C'EST NUL ! Je ne pourrais jamais utiliser un projet qui fonctionne de cette facon, je ne peux pas me permettre d'avoir un \u00e9cran inutilisable quand je commente et auquel je dois constamment faire attention pour ne pas perturber la reconnaissance. Pour moi cette option aurait \u00e9t\u00e9 celle \u00e0 choisir en cas d'extr\u00eame urgence et en dernier recours car le projet deviendrait inutile. J'ai donc d\u00e9cid\u00e9 de m'occuper de la seconde option : Simuler un navigateur. Cette option bien que complexe et difficile \u00e0 impl\u00e9menter propose une solution \u00e0 tous les probl\u00eame et permet une r\u00e9cup\u00e8ration quasi sans compromis. Simuler un navigateur ? Simuler un navigateur internet n'est pas forc\u00e9ment tr\u00e8s difficile. Chromium par exemple offre une panoplie d'outils natifs et \u00e9norm\u00e9ment de librairies existent permettant de facilement et en quelques lignes simuler un Google Chrome et le contr\u00f4ler sans afficher son UI. \"Chromium logo\" {: style=\"height:150px;width:150px\"} Cependant. La F1TV n'utilise pas simplement un player HTML5 basique. Elle utilise un service de streaming BitMovin qui permet de fournir un stream de bonne qualit\u00e9 et surtout qui impl\u00e9mente les DRM (Digital Right Management) Cela veut dire que quand on ouvre un flux de la F1TV sur chrome et que l'on essaie de prendre une capture d'\u00e9cran, le player se met en noir et ne permet pas de voir quoi que ce soit (Certaines version de Chrome le permettent pendant quelques semaines avant de bloquer \u00e0 nouveau). Ce qui dans notre cas est un immense probl\u00e8me. Mais Firefox ne nous bloque pas de cette facon et il est donc assez facile de passer outre. L'explication sans trop rentrer dans les d\u00e9tails est la suivante : Dans chrome, le player par d\u00e9faut utilise une technologie appell\u00e9e \"PCP\" ou \"Protected Content Playback\" qui leur permet de bloquer au moins une partie des techniques de r\u00e9cup\u00e8ration du flux vid\u00e9o et audio. Cependant Firefox de pas sa nature Open Source utilise \"OpenH264\" pour lire ces m\u00eames flux soumis \u00e0 des DRM et OpenH264 n'impl\u00e9mente pas les m\u00eames restrictions. Sauf que Firefox n'est pas aussi facilement \u00e9mul\u00e9 que chrome et cela r\u00e9duit notre choix de librairies \u00e0 ... Une seule... Qui est Selenium. (Il existe aussi Pupetteer C# mais j'ai rencontr\u00e9 \u00e9norm\u00e9ment de soucis avec cette derni\u00e8re d\u00e8s que je voulais lancer une vid\u00e9o) \"Firefox Developper logo\" {: style=\"height:150px;width:150px\"} Mais m\u00eame si la documentation est plut\u00f4t maigre parfois, c'est une bonne librairie qui permet de tr\u00e8s bien contr\u00f4ler une instance de chrome ou de Firefox. Contr\u00f4ler le navigateur Maintenant que l'on sait quel navigateur simuler et avec quelle technologie, on peut passer \u00e0 la r\u00e9alisation. Ce qu'il y a de bien avec Selenium, c'est qu'on a un certain nombre de commandes tr\u00e8s haut niveau qui nous permettent de contr\u00f4ler un navigateur de mani\u00e8re plut\u00f4t pr\u00e9cise. Je vais d\u00e9crire ici la proc\u00e9dure habituelle utilis\u00e9e sous une forme de recette de cuisine pour que l'on puisse facilement comprendre ce qu'il se passe. Durant cette explication je vais parler \u00e0 un moment de Cookies, ne vous en faites pas c'est le sous chapitre suivant qui va vous en parler. Recette de cuisine pour r\u00e9cup\u00e8rer des images de la F1TV : D\u00e9marrer une instance de navigateur avec les bons arguments Ajouter les bons param\u00eatres pour ne pas se faire flag comme un bot Naviguer sur la page de la F1TV Ajouter les cookies de connexion pour avoir acc\u00e8s au contenu de la page Naviguer sur la page du Grand Prix demand\u00e9 Attendre un peu que la page se charge Cliquer sur l'invite de cookies Attendre cinq secondes le temps que la page se reload Cliquer sur le bouton qui permet de passer du feed live \u00e0 la DATA CHANNEL Appuyer sur Espace pour faire apparaitre le bouton d'acc\u00e8s au param\u00eatres Cliquer sur le menu d\u00e9roulant des r\u00e9solution Trouver l'option 1080P et la selectionner Cliquer sur le bouton qui met la vid\u00e9o en plein \u00e9cran Prendre de screenshots \u00e0 intervales r\u00e9guliers Pour faire toutes ces actions on doit r\u00e9cup\u00e8rer les \u00e9l\u00e9ments selon leur ID ou leur classe. Voici un exemple qui r\u00e9cup\u00e8re le bouton de plein \u00e9cran et qui clique dessus : IWebElement fullScreenButton = Driver.FindElement(By.ClassName(\"bmpui-ui-fullscreentogglebutton\")); fullScreenButton.Click(); Ca peut para\u00eetre plut\u00f4t simple dit comme ca et quand tout fonctionne ca l'est mais la difficult\u00e9 vient du fait que \u00e0 peu pr\u00e8s nimporte laquelle de ces \u00e9tapes peut rater et qu'il faut donc faire un bon syst\u00e8me de gestion d'erreurs qui puisse aider l'utilisateur en cas de probl\u00e8me. Il faut dire aussi que les sites ne sont pas forc\u00e9ment tr\u00e8s content de voir des bots passer car cela peut \u00eatre un risque de DDOS et de Scraping (Comme moi) et donc ils mettent en place des syst\u00e8mes pour nous emp\u00eacher de faire ce que l'on veut On peut utiliser diff\u00e9rntes techniques pour passer outre ces restrictions comme : Changer son UserAgent Changer sa r\u00e9solution Ne pas avoir des patterns trop pr\u00e9visibles Avoir un historique Ne pas cliquer pile sur le milieu des boutons Ne pas cliquer trop vite Passer par un proxy pour ne pas se faire flag Utiliser des librairies plus discr\u00e8tes J'ai eu l'occasion de tester toutes ces methodes pour tenter de passer derri\u00e8re les radars de la F1TV et visiblement j'ai r\u00e9ussi pour les pages principales mais pas pour les pages de Login. Il faut savoir que la bataille entre bots et propri\u00e9taires de sites est un grand jeu du chat et de la souris et que les plateformes innovent constamment leur s\u00e9curit\u00e9. Et il se trouve que la partie login de la F1TV est heberg\u00e9e autre part que le reste du site chez Amazon et que elle poss\u00e8de les meilleures s\u00e9curit\u00e9s que j'aie pu voir. Aucunes des methodes que j'ai cit\u00e9es et d'autres encore que j'ai essay\u00e9 n'ont r\u00e9ussi \u00e0 fourvoyer le syst\u00e8me. J'ai donc \u00e9t\u00e9 oblig\u00e9 de faire appel \u00e0 la connexion par Cookies pour pouvoir acc\u00e8der au reste du site internet. R\u00e9cup\u00e8rer les cookies ? Alors, on va mettre de c\u00f4t\u00e9 toutes les questions de s\u00e9curit\u00e9 et de violation de la vie priv\u00e9e et de protection des donn\u00e9es des utilisateurs pour ce chapitre. Car pour faire simple, je siphonne TOUS les cookies de la persone qui utilise mon app. [FINIR CETTE EXPLICATIOn] Calibration [AJOUTER EXPLICATION] OCR Ici je vais parler de la seconde partie du projet qui parle du processus de reconnaissance de data sur une image du feed DATA de la F1TV. C'est je pense la partie qui a demand\u00e9 le plus tests et de refactor. Toute la partie OCR a \u00e9t\u00e9 d\u00e9velopp\u00e9e dans un projet \u00e0 part avant d'\u00eatre int\u00e9gr\u00e9e dans le projet final. Il faut savoir que la reconnaissance est diff\u00e9rente celon ce que l'on cherche. Je vais donc d\u00e9composer cette partie du document en sous rubriques selon les donn\u00e9es recherch\u00e9es. Mais avant ca je dois expliquer certains concepts qui seront importants. Fonctionnement g\u00e9n\u00e9ral Voici un screenshot de la page DATA de la F1TV que le programme va recevoir : \"Screen F1TV\" Si on regarde de loin on peut se dire que la structure est plut\u00f4t simple mais c'est loin d'\u00eatre le cas. On peut y voir au moins 4 zones contenant de l'information dans un format diff\u00e9rent. \"Main zones\" Dans l'exemple ci dessus on peut voir 3 zones mais on aurait \u00e9galement pu comprendre la zone de position des pilotes autour du circuit pour faire 4. Ces 4 zones sont tr\u00e8s diff\u00e9rentes et contiennent d'autres informations. Pour ce travail de dipl\u00f4me je ne m'occupe que de la zone principale. Mais je pense que le titre et les infos de circuit ne prendrait pas tant de temps que ca \u00e0 impl\u00e9menter. J'ai utilis\u00e9 le mot \"Zone\" plus haut et ca n'est pas juste un mot utilis\u00e9 au hasard. C'est le nom de l'objet que j'utilise pour les repr\u00e9senter dans mon programme. Mais comme c'est important de bien comprendre ce concept avant de continuer je vais vous l'expliquer. ZONE : L'objet \"Zone\" parent est un objet qui est une zone d'image. Je m'explique, le but d'une zone est d'\u00eatre un morceau d'une image plus grande. Le but d'une Zone est de contenir une liste de plus petites Zones ou bien une liste de \"Window\" (j'explique ce que c'est juste apr\u00e8s). Elle contient la portion d'image qui la concerne et ses propres dimensions. Le parent zone ne pr\u00e9voit que de pouvoir ajouter ou supprimer des \u00e9l\u00e9ments des listes de zones ou de windows ainsi qu'une methode qui permet d'aller chercher toutes informations des livres qu'elle contient. L'int\u00e9r\u00eat d'une zone est de pouvoir compartimenter une image dans des parties int\u00e9ressantes au niveau de la reconnaissance mais pas de traiter d'information. WINDOW : L'objet \"Window\" est un objet qui peut ressembler beaucoup \u00e0 l'objet \"Zone\". En effet elle aussi est une partie d'une image plus grande et contient ses dimensions, mais elle se distingue en deux points importants. Elle ne contient pas d'autres Zones ou Windows Elle peut retourner les informations \u00e9crites sur son image. Toutes les Window qui h\u00e9ritent du parent Window peuvent impl\u00e9menter une methode qui permet de renvoyer ce qui peut \u00eatre d\u00e9cod\u00e9 sur son image. Les enfants peuvent aussi aller piocher dans les nombresues methodes de r\u00e9cup\u00e8ration de donn\u00e9es contenues dans le parent Window. Mieux vaut r\u00e9utiliser le plus possible que de r\u00e9inventer la roue pour chaque Window. Une analogie un peu bancale pourrait se pr\u00e9senter comme la suivante : La zone est une armoire ou une bibliot\u00e8que. Si c'est une zone qui contient d'autres zones c'est une bibliot\u00e8que et chacune de ces sous-zones sont des armoires. Leur unique but est de contenir de mani\u00e8re ordonn\u00e9e des objets qui eux contiennent de l'information. Les livres ici sont les Windows. Ils contiennet de l'information et sont stock\u00e9s dans des armoires et on y acc\u00e8de en allant dans la bonne bibliot\u00e8que et en allant dans la bonne armoire. Derni\u00e8res choses pour comprendre le diagramme: Il existe une Main Zone qui est une des 4 grandes zones dont je parlais dans la d\u00e9composition de l'image. Il existe aussi des \"Driver Zone\" qui sont de plus petites zones contenues dans la Main Zone qui et qui ne contiennent que les informations d'un pilote. L'objet Window n'est quasi jamais utilis\u00e9, c'est presque tout le temps des enfants de Window plus sp\u00e9cifiques qui sont utilis\u00e9s, le but est que chaque type d'information sur l'image aie son type de window. Voila donc un petit diagramme qui montre le d\u00e9coupage du programme : \"Diagramme zones\" Pour visualiser encore un peu mieux comment ce d\u00e9coupage prend forme voici ce que chaque zone et Window contient. Main Zone : \"Main zone\" Driver Zone : \"Driver zone\" Driver Position Window : \"Driver position Window\" Driver name Window : \"Driver name window\" Driver LapTime Window : \"Driver Laptime window\" Driver Tyre Window : \"Driver tyre window\" Il existe d'autres types de Window mais ce sont les principaux. On se rend assez facilement compte que chacunes de ces windows va avoir besoin d'un traitement sp\u00e9cifique car la mani\u00e8re de reconnaitre le pneu utilis\u00e9 et le temps au tour ne peut pas \u00eatre la m\u00eame. Pour r\u00e9sumer, on a un programme qui prend en entr\u00e9e un fichier de configuration, qui prend des images de la F1TV et les d\u00e9coupe dans des ZONES qui elles m\u00eame sont d\u00e9coup\u00e9es en WINDOWS pour qu'on puisse plus facilement les d\u00e9coder. Maintenant qu'on a une liste de diff\u00e9rent types de zones on peut commencer \u00e0 chercher ce qu'il y a marqu\u00e9 dessus. Pour cela il faut dabord comprendre un petit peu comment l'OCR fonctionne et comment des libraries comme Tesseract fonctionnent pour donner du texte en partant d'une image. Pour faire tr\u00e8s simple, nous avons un mod\u00e8le qui est entrain\u00e9. C'est \u00e0 dire que on donne \u00e0 un programme un tr\u00e8s grand nombre de mots ou de lettres en lui disant ce que contiennent chaques images. Ensuite le programme va cr\u00e9er des matrices de convolutions pour chaque lettre avec comme objectif de detecter les points communs entre les lettres pour cr\u00e9er un alpphabet. Par exemple la matric de la lettre 'H' donnerait un poids important \u00e0 des lignes verticales connect\u00e9es par une ligne centrale. Et si on fournis assez de donn\u00e9es de bonne qualit\u00e9 au mod\u00e8le, les matrices peuvent \u00eatre tr\u00e8s efficace \u00e0 detecter si une lettre est un H ou un M. Il y a pleins d'autres methodes comme l'utilisation d'un dictionnaire de mots de la langue pour permettre la reconnaissance de mots m\u00eame si une lettre au milieu n'est pas comprise ou en ajoutant d'autres informations sur le contexte mais ca ne nous int\u00e9resse pas ici. C'est important de comprendre comment cette reconnaissance de caract\u00e8res avec des matrices fonctionne car cela va nous aider \u00e0 pr\u00e9parer nos donn\u00e9es pour lui rendre la vie facile et augmenter la pr\u00e9cision de nos r\u00e9sultats. Filtres et traitement On peut essayer de donner toutes nos images directement \u00e0 Tesseract pour qu'il reconnaisse tout le texte qu'il y voit mais on risque de se retrouver avec des r\u00e9sultats au mieux inconsistents. Dans notre cas, le soucis est que les chiffres et lettres sont beaucoup trop petits. Ils ne font parfoisd que 10 pixels de haut et cela fait que il n'est pas forc\u00e9ment facile de toujours les diff\u00e9rencier. De plus, comme ils sont petits, les art\u00e9facts d'aliasing sont assez violents et peuvent grandement d\u00e9former une lettre ou un chiffre. Exemple : Prenons le chiffre 9. Dans l'image il peut \u00eatre repr\u00e9sent\u00e9 de cette mani\u00e8re : \"Bad9Exemple\" On peut voir qu'il est flou, pour nous cela ne pose pas de probl\u00e8me et je pense que \u00e0 peu pr\u00e8s nimporte qui peut dire que c'est un 9. Cependant comme les contours sont flous et m\u00eame si on essaie de retirer le background : \"Aliased 9\" On voit que le 9 n'est pas clairement d\u00e9finit. En effet on pourrait le comprendre comme : \"First contour\" Ou comme : \"Second contour\" Voire m\u00eame simplement comme : \"Big contour\" Et on se rend bien compte que les performances de detection ne sont pas les m\u00eames dans ces trois cas. Il faut donc faire un certain post traitement des images pour supprimer les \u00e9l\u00e9ments parasites, les couleurs, et augmenter la visibilit\u00e9 des contours importants. Mais chaque type de donn\u00e9e va avoir des methodes de post traitement diff\u00e9rents. Donc voici les diff\u00e9rents types de reconnaissance et leur post traitements : Texte Alors ce type de reconnaissance est utilis\u00e9 par la WINDOW du nom de pilote et de la position du pilote. C'est je pense la plus simple de toutes car Tesseract est particuli\u00e8rement bien entrain\u00e9 pour. Cette reconnaissance concerne donc des lettres qui font des mots ou des noms. Voici un exemple de la WINDOW nom de pilote en entr\u00e9e : \"Exemple raw\" Ce texte peut paraitre bon, cependant quand on le lance dans Tesseract, il ne va pas toujours donner un r\u00e9sultat parfait. Il faut aussi savoir qu'il y a des noms pas mal plus p\u00e9nibles que Tesseract a plus de mal \u00e0 reconnaitres, soit \u00e0 cause des lettres utilis\u00e9es, soit car le nom est un nom d'une autre r\u00e9gion et qui ne veut rien dire en anglais ce qui emp\u00eache l'utilisation de dictionnaire (Ex : Tsunoda est un nom japonais et parfois il est difficile pour Tesseract de le reconnaitre car si une lettre pose probl\u00eame il ne peut pas trouver de contexte qui puisse l'aider). Donc pour le rendre plus facilement lisible et augmenter les chances que toutes les lettres soient d\u00e9couvertes, voici les \u00e9tapes que j'ai mis en place. 1 : J'inverse les couleurs. Je me suis rendu compte que il \u00e9tait souvent plus facile de trouver un noir sur blanc que blanc sur noir. Je ne suis pas sur que cette \u00e9tape soit capitale cependant. \"Texte invers\u00e9\" 2 : Je fais un Treshhold de 165 car avec moins le texte parfois prend trop du background et avec plus les lettres sont trop fines. \"Treshold\" 3 : Je fais un Resize de l'image pour avoir une meilleure r\u00e9solution et permettre une meilleure d\u00e9tection. J'augmente la hauteur et la largeur par un facteur 2. J'ai trouv\u00e9 cette valeur suffisante et aller plus haut consomme beaucoup de ressources. \"Resize\" 4: Je fais une tr\u00e8s rapide Dilatation du texte pour retirer le flou amen\u00e9 par la methode de Resize. Je n'utilise qu'une valeur de 1 car je ne veux pas trop changer comment le texte est model\u00e9 je veux juste retirer le flou. \"Dilatation\" Explication des methodes pr\u00e9cises plus bas Voila pour ce qui est du post processing. Je ne dis pas que ce sont les meilleurs param\u00eatres possibles mais dans mes tests ce sont ceux qui ont le mieux march\u00e9s. C'est aussi les premi\u00e8res methodes que j'ai pu d\u00e9velopper alors forc\u00e9ment elles n'ont pas le niveau de d\u00e9tails de certaines autres. Mais comme m\u00eame avec ce traitement il n'est pas rare que je me retrouve avec une ou deux lettres pas justes, il faut un moyen d'\u00eatre s\u00fbr que c'est le bon nom qui est trouv\u00e9. Ce qu'il y a de pratique avec les noms de pilotes c'est que on sait d\u00e9ja comment ils s'appellent avant le Grand Prix. En effet dans le fichier de configuration de la reconnaissance, il y a une liste de noms de pilotes. Cela veut dire que au lieu de chercher \u00e0 trouver parfaitement les bonnes lettres, on peut simplement essayer de trouver quel nom de pilote ressemble le plus au nom trouv\u00e9 sur l'image. Pour ce faire j'ai utilis\u00e9 une methode appel\u00e9e la distance de Levenshtein. Pour faire simple c'est une methode qui va calculer les distances de lettres pour determiner entre des strings laquelle ressemble le plus \u00e0 une autre. Pour r\u00e9sumer le fonctionnement dans lordre : On prend l'image on la traite On envoie l'image trait\u00e9e \u00e0 Tesseract On trouve quel nom de pilote ressemble le plus \u00e0 ce r\u00e9sultat On renvoie le nom du pilote Chiffres Cette methode en r\u00e9alit\u00e9 utilise simplement la m\u00eame methode que celle qui va r\u00e9cup\u00e8rer le texte sur une image. Cependant, la, on envoie \u00e0 Tesseract l'information qu'il ne peut trouver que des chiffres sur l'image ce qui lui permet d'\u00eatre beaucoup plus pr\u00e9cis et de ne pas confondre un 9 avec un P ou un 11 avec un H PAR EXEMPLE (non pas que ca me soit arriv\u00e9 tr\u00e8s r\u00e9guli\u00e8rement et que ca me soit rest\u00e9 dans la gorge \u00e9videmment) L'avantage c'est que cette methode ne demande m\u00eame pas de traitement de la donn\u00e9e en sortie de Tesseract. On \u00e9sp\u00e8re simplement que le post traitement aura suffit. TEMPS : Cette methode regroupe la d\u00e9tection de temps au tour. Il y a trois grands types de WINDOW qui sont concern\u00e9es : La WINDOW du temps au tour La WINDOW du retard sur le leader La WINDOW des secteurs La grande diff\u00e9rence ce sont les ordres de grandeur. Les temps au tour sont en g\u00e9n\u00e9ral entre 50 secondes et 2 minutes. Tandis que les secteurs sont entre 20 et 30 secondes alors que le retard sur le leader peut-\u00eatre de plusieurs minutes. Cependant, tous ces temps poss\u00e8dent le m\u00eame type de post-traitement avant d'\u00eatre envoy\u00e9s \u00e0 Tesseract. Voici un exemple de temps au tour avant toute transformation : \"Lap time\" On peut avoir l'impression que ce texte est tout \u00e0 fait lisible et facile \u00e0 d\u00e9coder surtout quand on le voit de loin comme ca. Cependant, il faut imaginer que ces chiffres font 13 pixels de haut en comptant le flou et comme expliqu\u00e9 plus haut ce flou dans ces echelles est terrible. \"Lap time\" Si on donne cette image \u00e0 Tesseract, les '3' deviennent des '9', des '9' deviennent des '8', des '2' deviennent eux aussi des '9', le tout parfois inversement et de mani\u00e8re compl\u00eatement impr\u00e9visible. Ca n'est simplement pas utilisable. Cette partie est un peu plus complexe car si la detection n'est pas fiable les chiffres sont simplement inutilisables. Si \u00e0 tout moment un temps au tour de 1:39.106 devient 1:32.108 c'est juste pas possible. Voici donc les \u00e9tapes de post-traitement que j'ai mis en place pour leur d\u00e9tection : 1: J'applique un Treshold de 185 pour enlever les ambiguit\u00e9s d'alisaising et avoir une image en noir et blanc claire. La valeur de 185 est assez \u00e9lev\u00e9e car le but est de vraiment garder uniquement les contours. Comme les chiffres se ressemlent beaucoup plu que les lettres, il faut tenter le plus possible de conserver leur formes sp\u00e9cifiques. Je me suis rendu compte que cette valeur \u00e9tait une de celles qui marchent le mieux. \"Treshold\" 2: J'applique un Resize de 2 pour augmenter la r\u00e9solution des chiffres et permettre une meilleure d\u00e9tection. Le but est d'avoir plus de pixels et donc de permettre \u00e0 Tesseract de mieux utiliser ses matrices de convolution. \"Resize\" 3: Comme le Resize am\u00e8ne du flou, j'utilise une methode de Dilatation qui me permet de retirer ce flou et de remplir un peu plus certaines parties qui ont \u00e9t\u00e9 un peu laiss\u00e9e par le Resize*; \"Dilatation\" 4: Contrairement aux mots plus haut, la rondeur ajout\u00e9e par la dilatation n'est pas vraiment d\u00e9sir\u00e9e. En effet, elle peut rendre confuse certains chiffres et emp\u00eacher Tesseract de bien trouver le chiffre. Alors j'applique une Erosion qui me permet de contrecarrer en partie les rondeurs ajout\u00e9es par la dilatation et retrouver des chiffres bien form\u00e9es. Pour l' Erosion et la Dilatation j'ai utilis\u00e9 une valeur de 1 car je ne voulais pas trop changer les chiffres. \"Erode\" Explication des methodes pr\u00e9cises plus bas Et avec ce post processing on retrouve de plut\u00f4ts bon r\u00e9sultats qui demandent peu de traitement. Le traitement d\u00e9pend du type de WINDOW cependant. Pour les secteurs on indique \u00e0 Tesseract que les caract\u00e8res autoris\u00e9s sont : \"0123456789.\" Pour les temps au tour on autorise plut\u00f4t \"0123456789.:\" Et pour les \u00e9carts on autorise \"0123456789.+\" Ensuite on r\u00e9cup\u00e8re une liste de chiffres qui'il va falloir transformer en milisecondes pour faciliter le stockage et l'envoi. Le programme nettoie un peu la chaine avant de la convertir. Par exemple parfois le ':' de 1:34.456 est compris comme un '1' ou un '2' et il faut faire attention \u00e0 detecter quand ca arriver. Je passe les d\u00e9tails du reste du nettoyage car c'est vraiment du cas par cas mais quand on a finit de nettoyer la chaine on peut transformer les chaines de minutes secondes et milisecondes en un total de milisecondes. Pour r\u00e9sumer le fonctionnement dans l'ordre : On prend l'image et on lui applique une s\u00e9rie de filtres On envoie l'image filtr\u00e9e \u00e0 Tesseract On nettoie le r\u00e9sultat Tesseract pour compenser certains biais On convertis le r\u00e9sultat en milisecondes Pneus La on arrive sur la partie la plus p\u00e9nible. Pour comprendre la probl\u00e9matique il faut d'abord faire un petit point sur comment les pneus fonctionnent en Formule 1. Depuis 2019 en Formule 1 nous avons 5 grandes familles de pneus : Les pneus tendres Les pneus medium Les pneus durs Les pneus interm\u00e9diaires Les pneus pluie \"Tyres\" Les trois premiers pneus sont des pneus faits pour piste s\u00e8che, le pneu interm\u00e9diaire pour piste humide et le neu pluie pour la pluie. Chaque pneu a sa dur\u00e9e de vie et son niveau de performance propre mais je ne vais pas rentrer dans le d\u00e9tail ici. Tout ce qu'il faut savoir ce que savoir sur quel pneu chaque pilote est et depuis combien de temps il les chausse est une information tr\u00e8s importante. Chaque pneu a une couleur donn\u00e9e qui permet de les diff\u00e9rencier. Voici un exemple de ce \u00e0 quoi une WINDOW de pneus peut ressembler : \"Exemple 1\" Mais cette zone peut aussi ressembler \u00e0 ca : \"Exemple 2\" Mais aussi \u00e0 ca : \"Exemple 3\" Voire m\u00eame ca : \"Exemple 4\" Je pense que vous pouvez tout de suite comprendre la difficult\u00e9 que repr\u00e9sente la t\u00e2che de r\u00e9cup\u00e8ration de donn\u00e9es \u00e0 partir de cette image. En gros le fonctionnement de cette zone d'information est assez simple. Au fur et \u00e0 mesure que la course avance, le trait fait de m\u00eame. Le chiffre dans le round tout \u00e0 droite indique le nombre de tour que le pilote a pass\u00e9 sur ce pneu. La couleur indique le type de pneu. Si il y a une lettre \u00e0 la place d'un chiffre c'est que c'est le premier tour sur ce pneu. La lettre indique le type de pneu. Et pas besoin de dire que si on essaie simplement de donner l'image \u00e0 Tesseract on ne r\u00e9cup\u00e8re ni les chiffres ni les lettres correctement si ce n'est pas du tout. Il faut donc utiliser une methode qui permette d'isoler le rond le plus \u00e0 droite, lui appliquer un traitement qui permette \u00e0 Tesseract de lire ce qu'il y a marqu\u00e9 et qui puisse determiner quel pneu est en train d'\u00eatre utilis\u00e9. J'ai d\u00e9cid\u00e9 de m'occuper dans un premier temps de trouver ce rond avant d'appliquer les filtres car plus l'image est petite plus les filtres sont rapides. Le programme va tirer un trait depuis le bord droit de la zone, et il va avancer vers la gauche jusqu'\u00e0 trouver un obstacle. Je d\u00e9tecte un obstacle si le pixel sur lequel est mon trait poss\u00e8de une valeur de plus de 0x50 dans le channel R,G ou B. J'ai trouv\u00e9 en faisant des tests que les couleurs de background de la F1TV ne d\u00e9passaient jamais ces valeurs. Ensuite apr\u00e8s avoir trouv\u00e9 le premier obstacle, je r\u00e9cup\u00e8re une zone qui doit englober le cercle. Voici un exemple avec cette image en entr\u00e9e : \"Full zone\" Elle est automatiquement coup\u00e9e de cette facon : \"Cropped zone\" Cela me permet d'isoler uniquement ce qui m'int\u00e9resse ce qui est tr\u00e8s pratique pour Tesseract et pour la detection de couleur. Ensuite avec cette image je peux commencer le processus de reconnaissance. Je commence par faire une moyenne de tous les pixels de l'image en excluant les pixels trop sombres qui font s\u00fbrement partie du background ou du chiffre. Ensuite j'utilise une methode qui calcule la diff\u00e9rence entre la couleur obbtenue et la liste de couleurs possible. Il y a cinq couleurs des pneus possibles : \"#ff0000\" pneu tendre/soft \"Soft tyre color\" \"#f5bf00\" pneu medium \"medium tyre color\" \"#a4a5a8\" pneu dur/hard \"Hard tyre color\" \"#00a42e\" pneu inter \"Inter tyre color\" \"#2760a6\" pneu pluie/wet \"Wet tyre color\" Ce qui est pratique c'est que m\u00eame dans les cas ou il n'y a pas beaucoup de couleur comme celui la : \"Hard tyre but only the letter\" On arrive \u00e0 une couleur moyenne de : \"The average color from the picture above\" Et il est donc assez facile de determiner le type de pneu en question. Attention, les r\u00e9sultats peuvent \u00eatre tr\u00e8s vite d\u00e9rang\u00e9s par la couleur du pneu pr\u00e9c\u00e9dent si le d\u00e9coupage de la fen\u00eatre n'a pas \u00e9t\u00e9 assez pr\u00e9cis. Ensuite il \"suffit\" de lire le chiffre dans le rond et si on arrive pas \u00e0 le lire alors c'est que c'est une lettre et on sait que le nombre de tours est donc de 0. Maintenant vient le moment tr\u00e8s sympatique de la lecture du chiffre. Vous saurez que Tesseract en plus de detester les grandes images et les images avec des couleurs, deteste \u00e9galement les formes dans une image. Donc dans notre cas, le round de couleur autour du chiffre, m\u00eame si il n'est pas complet, il interf\u00e8re avec la reconnaissance et emp\u00eache de bien lire le chiffre. Il faut donc retirer le background et ensuite la couleur. Sauf que comme le chiffre est de la couleur du background, si on retire le background et ensuite la couleur il ne reste plus rien. Il faut donc retirer le background AUTOUR du rond, et ensuite si on retire la couleur il devrait rester le chiffre sur fond blanc. Pour se faire, j'ai tir\u00e9 des traits depuis les bords de l'image jusqu'\u00e0 ce qu'ils rencontrent le rond. Ensuite je retire tous les pixels entre le rond et les bords de l'image ce qui nous donne ceci : \"No outer background\" Ensuite on peu retirer les pixels qui ont une valeur dans un channel RGB plus haute qu'un certain seuil : \"Only digit\" Et la on a ce que l'on veut ! A partir de la c'est les filtres que l'on connait qui sont utilis\u00e9s pour en faire une image plus facile \u00e0 utiliser par Tesseract. 1 : On effectue un Resize de facteur 4 (oui c'est beaucoup mais en m\u00eame temps le chiffre est vraiment petit \u00e0 la base) qui permet d'avoir une image d'une bien meilleure r\u00e9solution. \"Filter 1\" 2: On fait une Dilatation de facteur 1 pour retirer tout le flou de l'image pour aider Tesseract \"Result\" Et on a un chiffre qui est utilisable par Tesseract ! Explication des methodes pr\u00e9cises plus bas Pour r\u00e9sumer : On prend l'image de la zone et on la crop pour ne garder que la partie essentielle On d\u00e9termine le type de pneu avec la couleur moyenne de la zone On retire le background autour de cette zone On retire la couleur qui reste pour ne garder que le chiffre On augmente la r\u00e9solution du chiffre On rend ce chiffre net On envoie l'image trait\u00e9e et filtr\u00e9e \u00e0 Tesseract On d\u00e9termine le nombre de tours que le pilote a fait avec ses pneus avec le r\u00e9sultat de Tesseract DRS Bon ca c'\u00e9tait plut\u00f4t simple j'ai simplement v\u00e9rifi\u00e9 si la moyenne de vert d\u00e9passait une certaine valeur et puis voila. Filtres et methodes sur les images Dans ce projet on a du utiliser diff\u00e9rentes methodes d'\u00e9dition d'image que ce soit sous forme de filtres ou de modification de l'image directement. Voici un sommaire des methodes utilis\u00e9es et comment elles fonctionnent. Tresholding Cette methode sert \u00e0 passer d'une image en couleurs \u00e0 une image binaire noir blanc. C'est une \u00e9tape tr\u00e8s importante pour l'OCR car elle permet (si bien faite) d'isoler du texte de son background. Un exemple ici : \"Exemple treshold\" Le fonctionnement est assez simple mais il peut \u00eatre fait de diff\u00e9rentes mani\u00e8res mais dans mon cas voici comment l'algorythme fonctionne sachant qu'il demande en entr\u00e9e la Bitmap que l'on veut modifier ainsi que la valeur de Treshold : On parcours chaque pixel de l'image On convertir la couleur du pixel en une valeur de gris pour avoir la m\u00eame valeur en R,G et B (Formule utilis\u00e9e : grey = R x 0.3 + G x 0.59 + B x 0.11) Si le r\u00e9sultat de la valeur de gris est au dessus de la valeur de treshold, le pixel est pass\u00e9 en blanc complet et dans le cas contraire il est pass\u00e9 en noir complet On retourne la Bitmap modifi\u00e9e Un algorythme pas forc\u00e9ment complexe mais qui peut augmenter de mani\u00e8re titanesque les chances de r\u00e9ussir une OCR Resize Cette methode sert \u00e0 augmenter la r\u00e9solution d'une image pour am\u00e9liorer la pr\u00e9cision de l'algorythme de Tesseract. En effet, avec trop peu de pixels, la matrice de convolution n'est pas toujours aussi efficace. Il ne faut pas confondre cette methode d'augmentation de la taille avec une simple interpolation. En effet une augmentation de taille interpol\u00e9e ne vas pas vraiment changer la r\u00e9solution, l'image sera toujours aussi pixelis\u00e9e, seulement, les pixels seront compos\u00e9es de plus de pixels comme dans l'exemple ci dessous : \"Interpolation exemple\" Dans mon projet j'utilise de l'interpolation bicubique qui va cr\u00e9er de l'information pour tenter de combler le vide et produire une image r\u00e9ellement plus grande et avec plus de details mais en ajoutant du flou. \"bicubic exemple\" Le but est d'aller chercher dans les pixels alentours les couleurs qui sont d\u00e9ja pr\u00e9sente et de jouer avec des poids pour tenter de faire une pr\u00e9diction de ce que ce pixel aurait \u00e9t\u00e9 si l'image avait plus de detail. Voici un exemple assez parlant : \"bicubic demonstration\" \"bicubic demonstration\" On pourrait croire que c'est inutile mais dans le contexte de Tesseract ajouter des d\u00e9tails pour tenter de simuler une meilleure r\u00e9solution m\u00eame en cr\u00e9ant du flou est int\u00e9ressant pour mieux remplir la matrice de convolution. Mais il est possible de r\u00e9duire ce flou avec d'autres m\u00e9thodes \u00e9galement. (Dans mon code je n'ai pas utilis\u00e9 du code fait main mais j'utilise une librairie qui me permet de le faire) Il faut simplement faire attention car c'est un proc\u00e9d\u00e9 assez lourd en performances. Dilatation et Erosion Cette methode et la suivante font partie des methodes de transformation morphologiques. Ces methodes sont utilis\u00e9es pour accentuer les formes et les epaissir ou les r\u00e9duire et les affiner. Elles poss\u00e8dent l'aventage \u00e9galement de retirer le flou d'une image ce qui est tr\u00e8s pratique si utilis\u00e9 apr\u00e8s l'utilisation de methodes comme Resize . Je ne vais pas trop rentrer dans les d\u00e9tails de ces methodes car leur fonctionnement est un peu plus lourd en math si on veut faire une v\u00e9ritable explication du pourquoi et du comment ca marche aussi bien. Pour notre projet je dirais que l'important est de savoir que ce sont deux outils tr\u00e8s pratiques pour changer la morphologie des lettres et des chiffres et qu'on peut les utiliser pour corriger du flou et/ou des art\u00e9facts apparus lors de la binarisation de l'image ou de la suppression de fond. Remove Background Cette methode est assez simple et est juste une methode qui va passer en revue tous les pixels de l'image et si la couleur d'un pixel s'apparente \u00e0 celle d'un pixel de fond il est pass\u00e9 en noir total ou en blanc total. Le but est de permettre au reste du programme de fonctionner avec des couleurs moins ambigues. Une variante sp\u00e9cialis\u00e9e pour la reconnaissance des pneus appel\u00e9e affectueusement Remove Useless cherche \u00e0 atteindre le m\u00eame bu mais est bien plus soffistiqu\u00e9e et sp\u00e9cialis\u00e9e pour retirer le background autour d'un cercle de couleur pour ensuite retirer la couleur et qu'il ne reste qu'un chiffre. Pour plus de details voir la detection de pneus. Il y aussi d'autre methodes comme un filtre Gaussien ou Highlight countour que j'ai du d\u00e9velopper mais que je n'ai pas utilis\u00e9 donc je ne vais pas en parler ici. Interpr\u00e9tation des donn\u00e9es Stockage des donn\u00e9es Affichage des donn\u00e9es Pr\u00e9dictions Tests [A remplir au fur et \u00e0 mesure de la cr\u00e9ation des tests] R\u00e9sum\u00e9 des difficult\u00e9s techniques [A remplir au fur et \u00e0 mesure dans la seconde moiti\u00e9 du travail de dipl\u00f4me] Am\u00e9liorations futures [A remplir dans les derni\u00e8res semaines du travail de dipl\u00f4me] Conclusion [A remplir la derni\u00e8re semaine du travail de dipl\u00f4me]","title":"Rapport Track Trends V1.0"},{"location":"index.html#rapport-track-trends-v10","text":"Rohmer Maxime Travail de dipl\u00f4me Technicien ES 2023","title":"Rapport Track Trends V1.0"},{"location":"index.html#introduction","text":"","title":"Introduction"},{"location":"index.html#resume","text":"Track Trends est un outil de r\u00e9cup\u00e9ration et d'analyse de donn\u00e9es de courses de Formule 1. Pour le contexte, en dehors des cours, j'exerce diff\u00e9rentes activit\u00e9s dont celle de Live Ticker F1 pour le 20 minutes. Pour m'aider dans ce travail, j'utilise actuellement la F1TV \u00e0 laquelle je suis abonn\u00e9 qui me propose non seulement un feed vid\u00e9o de meilleure qualit\u00e9 avec des commentaires plus pertinents que ceux de la RTS mais qui aussi me permet d'acc\u00e9der \u00e0 un feed vid\u00e9o tr\u00e8s important : la chaine data. Ce dernier ressemble \u00e0 cela : \"Screenshot du feed data de la f1tv\" (Attention ce n'est pas un joli tableau HTML, mais bien une vid\u00e9o qui contient un tableau.) Sauf que toutes les informations sont \u00e9tal\u00e9es p\u00eale-m\u00eale sans hi\u00e9rarchie ce qui fait que cela me prendrait trop de temps de tout d\u00e9chiffrer \u00e0 chaque fois, ce qui me fait rater des choses int\u00e9ressantes. Le but du projet est donc de fournir un outil qui hi\u00e9rarchise et affiche diff\u00e9remment les donn\u00e9es pour faciliter leur lecture et me permettre de faire de meilleurs commentaires.","title":"R\u00e9sum\u00e9"},{"location":"index.html#abstract","text":"Track Trends is a Formula 1 data and analysis tool. To understand everything, a little bit of context. In my free time I have multiple activities and one is to be the Live Ticker F1 for the local journal \"20 minutes\". to help me in this work I'm currently using the F1TV to which I'm currently subscribed because it provides me with a better video feed with better commentary than the ones from the RTS (in my opinion) but also because it gives me access to a very important video feed : the data channel See the screenshot above to see what it looks like. [note: It's a pretty HTML table but a full on video feed that contains a table (probably, so you can't access data directly)] You can see a lot of data all well and good BUT! All the data is displayed the same in a big table which make it really hard to read totally in a hurry, which means that I miss a lot of useful information. The point of the project then is to provide with a tool that can display those data by taking into account their relevance. That would help me not miss any and provide a better commentary by never missing out battles, and be able to better write with the time I saved by using it.","title":"Abstract"},{"location":"index.html#description-du-besoin","text":"Comme expliqu\u00e9 dans le r\u00e9sum\u00e9, je suis Live Ticker F1. Mais pour mieux comprendre le besoin que j'ai, je pense qu'il est pertinent de comprendre comment je travaille. Pendant un Grand Prix de Formule 1 j'ai plusieurs t\u00e2ches \u00e0 effectuer : Suivre les diff\u00e9rents \u00e9v\u00e8nements du Grand Prix Changer le titre et la photo de titre du Live Chercher des Tweets ou des Images \u00e0 int\u00e9grer Ecrire les commentaires en faisant attention \u00e0 dire ce qu'il se passe mais aussi l\u2019expliquer, ce que cela implique, mais aussi ce que cela veut dire pour le reste de la course. Comprendre et expliquer les strat\u00e9gies Tout cela toutes les cinq minutes max... Cela veut dire que je dois \u00eatre le plus rapide possible quand je cherche des informations. Et comme le tableau en comporte trop et bien, je suis oblig\u00e9 de le lire en diagonale. Par exemple dans le tableau, les infos que je cherche le plus sont : Le nombre de places gagn\u00e9es (surtout au d\u00e9part) Les \u00e9carts entre les pilotes (surtout ceux qui sont en dessous de deux secondes). Les pneus de chaque pilote et combien de tours, ils ont fait dessus Les temps d'arr\u00eats aux stands Les temps au tour (surtout pour la strat\u00e9gie) Mais pleins d'autres informations existent si on les recoupe sur plusieurs tours. Un outil qui permettrait de mettre en \u00e9vidence les informations importantes serait donc une tr\u00e8s grosse plus-value pour mon travail et s'il est facile \u00e0 installer et \u00e0 utiliser, il se pourrait qu'il devienne indispensable.","title":"Description du besoin"},{"location":"index.html#cahier-des-charges","text":"Il s'agit d'une version coup\u00e9e du cahier des charges qui ne contient pas l'explication du contexte. Mais l'original est disponible sur ce site \u00e9galement. Il est toutefois normal d'y voir des choses r\u00e9p\u00e9t\u00e9es ou l\u00e9g\u00e8rement diff\u00e9rentes, en effet, il n'a pas \u00e9t\u00e9 \u00e9crit en m\u00eame temps que le reste de ce document.","title":"Cahier des charges"},{"location":"index.html#projet","text":"Un outil de style compagnon sous forme d'application C# Windows Form qui r\u00e9cup\u00e8re en temps r\u00e9el les informations de la course et affiche les informations les plus importantes. Le but est non seulement de faciliter mon job, mais aussi faire en sorte d'am\u00e9liorer la plus-value de mon travail en me permettant de fournir des commentaires qui ne sont pas disponibles pour le tout venant qui regarde simplement le flux RTS. Exemples : Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand Maintenant afficher diff\u00e9remment les infos, c'est sympa, mais cela serait encore mieux de traiter ces data et de permettre des petites pr\u00e9dictions. Exemples : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents","title":"Projet"},{"location":"index.html#realisation","text":"Malheureusement, la Formula 1 Management ne propose aucune API publique qui puisse nous permettre de faire ce projet \"simplement\". La raison la plus probable \u00e9tant qu'Amazon avec son service AWS propose exactement ce genre de services pour le flux t\u00e9l\u00e9vis\u00e9 et il doit y avoir un contrat d'exclusivit\u00e9. Il existe des API \"Pirates\" faites par la communaut\u00e9, le probl\u00e8me est qu'elles ne sont pas forc\u00e9ment des plus pratiques \u00e0 utiliser. Mais comme je poss\u00e8de un abonnement premium ++ \u00e0 la F1TV, j'ai acc\u00e8s pour chaque grand prix \u00e0 un flux vid\u00e9o nomm\u00e9 : DATA F1 CHANNEL Qui ressemble \u00e0 \u00e7a : \"Data channel exemple\" Donc la seule fa\u00e7on que je vois de r\u00e9cup\u00e9rer ces donn\u00e9es est de les prendre directement sur ce feed. M\u00eame si le but final de l'application est de faire pleins de choses super avec les datas, le gros du projet va surtout \u00eatre la r\u00e9cup\u00e9ration des donn\u00e9es et leur stockage. Les donn\u00e9es viennent du flux vid\u00e9o et ainsi dans un premier temps, il va falloir r\u00e9cup\u00e9rer d'une mani\u00e8re ou d'une autre des images qui viennent d'un grand prix en direct ou en rediffusion. Ensuite, dans un second temps, il faut lire les informations directement sur l'image en utilisant une librairie pr\u00e9vue pour (exemple Tesseract) et v\u00e9rifier l'int\u00e9grit\u00e9 de ces derni\u00e8res pour qu'on puisse ensuite les stocker. Dans un troisi\u00e8me temps, il faut stocker toutes ces donn\u00e9es dans une forme qui permette d'aller facilement faire des requ\u00eates de r\u00e9cup\u00e9ration et d\u00e9j\u00e0 pr\u00e9parer des m\u00e9thodes qui permettent de r\u00e9cup\u00e9rer des infos importantes (ex : la moyenne des cinq derniers tours, le temps moyen d'arr\u00eat etc.) pour faciliter la derni\u00e8re \u00e9tape Quand tout cela est fait, on peut ensuite s'amuser un peu avec les Data. La derni\u00e8re \u00e9tape est donc l'affichage. L'id\u00e9e est de cr\u00e9er une Windows Form qui contienne toutes ces informations dans un format beaucoup plus lisible et avec laquelle on pourrait interagir pour permettre de plus facilement commenter les Grands Prix. (exemple plus bas avec un croquis de ce \u00e0 quoi l'application pourrait ressembler) Voici la liste des donn\u00e9es qui pourraient \u00eatre affich\u00e9es (Non contractuel, simplement des id\u00e9es). Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand La moyenne de temps que les pilotes perdent dans les stands La performance moyenne des 5 types de pneus La moyenne de temps de chaque pilote sur le pneu actuel Le nombre de points que chaque pilote gagnerait selon sa position Le classement de la course Voire m\u00eame si c'est possible : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents Pr\u00e9dire les temps au tour de chaque pilote selon l'usure des pneus Voici un exemple d'interface possible pour une page : \"Proto\"","title":"R\u00e9alisation"},{"location":"index.html#cas-dutilisation","text":"'*'On va consid\u00e9rer que tous les user ont un abonnement F1 TV PRO Un user veut r\u00e9cup\u00e9rer les data : Il ouvre son navigateur et lance la page DATA de la F1 TV Il calibre la capture des data via le programme (pour la premi\u00e8re utilisation). Il confirme que les donn\u00e9es initiales sont bonnes (pour la premi\u00e8re utilisation). Il regarde tranquille son Grand Prix Le programme r\u00e9cup\u00e8re les data : Il r\u00e9cup\u00e8re des images depuis la F1TV Il utilise Tesseract (ou autre) pour en r\u00e9cup\u00e9rer les infos. Il met ces infos dans un Objet Pilote, dans un Objet course avec un attribut tour pour hi\u00e9rarchiser les data Pour ce qui est de l'affichage, l'id\u00e9e est de faire une application C# comme on l'a appris \u00e0 l'\u00e9cole, mais avec assez de style pour qu'elle puisse \u00eatre agr\u00e9able \u00e0 utiliser. Quand le programme affiche les data : Il prend les donn\u00e9es venant directement de la F1TV. Il affiche diff\u00e9remment les donn\u00e9es pour permettre une meilleure lisibilit\u00e9 Il interpr\u00e8te avec des r\u00e8gles plut\u00f4t simples certaines data pour faire des minipr\u00e9dictions ou aider \u00e0 la lecture Il r\u00e9cup\u00e8re des infos d'autres courses pour les comparer et proposer des pr\u00e9dictions plus int\u00e9ressantes","title":"Cas d'utilisation"},{"location":"index.html#difficultes-techniques","text":"R\u00e9cup\u00e9rer un flux vid\u00e9o plut\u00f4t propre malgr\u00e9 les contres mesures de la F1 TV pour en emp\u00eacher la lecture par un logiciel Si on doit passer par une capture d'\u00e9cran, trouver un moyen de stocker les donn\u00e9es de mani\u00e8re \u00e0 pr\u00e9voir que parfois un tour pourrait avoir plus de donn\u00e9es qu'un autre, que le user peut mettre pause, ou m\u00eame qu\u2019il revienne en arri\u00e8re. D\u00e9velopper des algorithmes pour r\u00e9cup\u00e9rer les donn\u00e9es comme les diff\u00e9rents pneus utilis\u00e9s ou l'activation du DRS ainsi que d\u00e9velopper des moyens de nettoyer les r\u00e9sultats de l'OCR (Par exemple utiliser diff\u00e9rents champs redondants pour comparer les r\u00e9sultats) Stocker les donn\u00e9es sur une base pour les traiter plus tard tout en pr\u00e9voyant un moyen de voir les stats live D\u00e9velopper des algorithmes de pr\u00e9diction qui prennent en compte d'anciennes courses pour tenter de pr\u00e9dire des choses comme les arr\u00eats aux stands par exemple.","title":"Difficult\u00e9s techniques"},{"location":"index.html#differences-sur-le-cahier-des-charges","text":"[\u00c0 remplir dans les derni\u00e8res semaines du travail de dipl\u00f4me]","title":"Diff\u00e9rences sur le cahier des charges"},{"location":"index.html#planning-previsionnel","text":"Mes suiveurs m'ont demand\u00e9 un planning de type GANTT pour ce travail de dipl\u00f4me Je n'ai pas utilis\u00e9 un logiciel particulier pour faire ce dernier, mais je me suis inspir\u00e9 des principes fondamentaux d'un diagramme de ce type. Comme l'original a \u00e9t\u00e9 fait sur Excel, je ne peux pas vraiment l'ins\u00e9rer de mani\u00e8re lisible ici, mais il est disponible dans le dossier Planning. Mais voici un r\u00e9sum\u00e9 de son contenu :","title":"Planning pr\u00e9visionnel"},{"location":"index.html#taches","text":"J'ai d\u00e9cid\u00e9 de d\u00e9composer mon planning en trois grands types de t\u00e2ches. Programmation Documentation Tests L'id\u00e9e est de permettre une meilleure lisibilit\u00e9 et de me permettre \u00e0 moi de me faire plus facilement \u00e0 l'id\u00e9e de ce qu'il m'attend. Voici la liste des t\u00e2ches par rubrique :","title":"T\u00e2ches"},{"location":"index.html#pt","text":"Cette rubrique contient les t\u00e2ches qui n'ont pas leur place dans les trois cat\u00e9gories principales.","title":"PT"},{"location":"index.html#pt1-preparation-au-travail-de-diplome-2","text":"Cette t\u00e2che est un peu hors cat\u00e9gorie, mais c'est normal, c'est une supert\u00e2che qui regroupe beaucoup de choses. C'est une t\u00e2che qui est planifi\u00e9e pour deux jours et qui normalement devrait \u00eatre faite les deux premiers jours du travail. Le but est de pr\u00e9parer tout ce qui peut \u00eatre pr\u00e9par\u00e9 en avance niveau documentation et mise en place pour ne pas avoir besoin de s'en soucier ensuite.","title":"PT1 / pr\u00e9paration au travail de dipl\u00f4me (2)"},{"location":"index.html#dt","text":"Rubrique documentation qui contient toutes les t\u00e2ches en rapport de pr\u00e8s ou de loin avec la documentation du projet.","title":"DT"},{"location":"index.html#dt1-creation-du-poster-1","text":"Cette t\u00e2che consiste \u00e0 faire une version num\u00e9rique du poster qui soit en accord avec les consignes qu'on nous a donn\u00e9es. Le but est aussi et surtout de faire poster dont je sois fier et que je sois content de montrer. Il y a d\u00e9j\u00e0 des croquis de poster et j'ai clairement pr\u00e9vu de travailler sur \u00e7a pendant les vacances alors, je n'ai mis qu'un jour et je l'ai plac\u00e9 juste avant le rendu de ce dernier.","title":"DT1 Cr\u00e9ation du poster (1)"},{"location":"index.html#dt2-documentation-analyse-de-lexistant-2","text":"Cette t\u00e2che est d\u00e9di\u00e9e \u00e0 l'\u00e9criture de la documentation et plus pr\u00e9cis\u00e9ment de l'analyse de l'existant. Comme il y a pas mal de technologies utilis\u00e9es dans mon projet, j'aimerais faire correctement un vrai debrief de pourquoi j'ai utilis\u00e9 l'une ou l'autre alors, j'ai assign\u00e9 deux jours dessus.","title":"DT2 Documentation Analyse de l'existant (2)"},{"location":"index.html#dt3-documentation-analyse-organique-5","text":"Cette t\u00e2che est la plus grosse dans la cat\u00e9gorie documentation. Il s'agit de documenter comment l'application fonctionne. J'y ai mis cinq jours et je pense que c'est un minimum car c'est dans cette t\u00e2che que je vais devoir d\u00e9tailler exactement comment fonctionne chaque partie du projet. Ces cinq jours sont \u00e9parpill\u00e9s sur le projet en g\u00e9n\u00e9ral \u00e0 la fin du d\u00e9veloppement de chaque grande partie de projet. Le but est de ne rien oublier et de ne pas avoir \u00e0 tout faire en m\u00eame temps.","title":"DT3 Documentation Analyse organique (5)"},{"location":"index.html#dt4-documentation-analyse-fonctionnelle-2","text":"Cette t\u00e2che est d\u00e9j\u00e0 moins grosse, elle consiste \u00e0 documenter le fonctionnement de l'application et comment utiliser les composants que j'ai d\u00e9velopp\u00e9s. Je l'ai mis en fin de projet, car comme j'ai l'habitude de faire des analyses fonctionnelles plut\u00f4t pr\u00e9cises, le moindre changement dans l'UI peut tout rendre obsol\u00e8te. J'y ai mis deux jours, car j'aimerais correctement documenter avec de bonnes photos et sc\u00e9narios pour qu'on puisse voir toutes les possibilit\u00e9s de l'application.","title":"DT4 Documentation Analyse fonctionnelle (2)"},{"location":"index.html#dt5-documentation-tests-1","text":"Cette t\u00e2che est un peu plus petite qu'elle ne le devrait. Elle concerne la documentation des diff\u00e9rents tests. Je n'y ai mis qu'un seul jour, car en r\u00e9alit\u00e9 les diff\u00e9rentes t\u00e2ches de tests contiennent aussi beaucoup de documentation,","title":"DT5 Documentation Tests (1)"},{"location":"index.html#dt6-documentation-reste-2","text":"Cette t\u00e2che est une t\u00e2che un peu vague. Elle contient toutes les actions autres que j'aurai besoin de faire (Mise au propre, orthographe, g\u00e9n\u00e9ration de PDF ...). J'y ai mis deux jours pour avoir un peu de marge, car ce sont toujours des t\u00e2ches qui paraissent faciles, mais qui \u00e0 la fin prennent beaucoup de temps si on les fait bien.","title":"DT6 Documentation Reste (2)"},{"location":"index.html#pt_1","text":"Rubrique programmation qui contient toutes les t\u00e2ches qui touchent \u00e0 la programmation et au d\u00e9veloppement de l'application.","title":"PT"},{"location":"index.html#pt1-programmation-recuperation-des-images-3","text":"Cette t\u00e2che est estim\u00e9e \u00e0 seulement trois jours, il ne faut pas s'y m\u00e9prendre, c'est une des t\u00e2ches les plus dures et lourdes niveaux documentation en explications. Cependant, un POC (Proof Of Concept) assez avanc\u00e9 a d\u00e9j\u00e0 \u00e9t\u00e9 fait et donc cela permet de n'envisager que trois jours, car il suffit de l'impl\u00e9menter et de la paufinner. Cette t\u00e2che consiste \u00e0 prendre en entr\u00e9e un lien de Grand Prix et de sortir une image tous les x secondes de la page DATA. Cela peut sembler simple, mais pour le faire sans prendre d'espace d'\u00e9cran et ne demandant pas \u00e0 l'utilisateur de copier-coller quoi que ce soit o\u00f9 de donner ses identifiants F1TV c'est un challenge. Cela peut paraitre curieux alors de mettre cette t\u00e2che loin dans le planning m\u00eame si c'est la premi\u00e8re \u00e9tape du projet. Encore une fois cela s'explique avec le fait qu'il y a d\u00e9j\u00e0 un POC qui fonctionne \u00e0 peu pr\u00e8s et que donc pr\u00e9f\u00e8re commencer avec des t\u00e2ches plus incertaines dans le cas o\u00f9 elles prendraient plus de temps que pr\u00e9vu.","title":"PT1 Programmation r\u00e9cup\u00e9ration des images (3)"},{"location":"index.html#pt2-programmation-ocr-5","text":"Cette t\u00e2che consiste \u00e0 d\u00e9velopper la partie qui reconnait le texte sur les images. C'est tr\u00e8s certainement la t\u00e2che qui risque le plus de d\u00e9border car c'est celle qui est la plus complexe techniquement puisqu'elle demande non seulement la lecture sur image, mais aussi le d\u00e9veloppement d'algorithmes de traitement de cette donn\u00e9e pour \u00eatre s\u00fbr qu'elle a bien \u00e9t\u00e9 lue. J'y ai ainsi allou\u00e9 cinq jours, mais j'esp\u00e8re que j'arriverai \u00e0 gagner du temps sur les autres pour y allouer plus dans le planning effectif, car je suis convaincu que plus, on y passe du temps, meilleur sera le r\u00e9sultat.","title":"PT2 Programmation OCR (5)"},{"location":"index.html#pt3-programmation-stockage-et-modele-5","text":"Cette partie est moins technique, mais concerne le stockage des donn\u00e9es que nous retourne la lecture des images. Mais elle va demander de la r\u00e9flexion et de l'intelligence de programmation, car il faut que cette partie anticipe les besoins de la vue et pr\u00e9pare un terrain fertile qui ne demande pas un refactor quand on passera au d\u00e9veloppement de la vue. C'est pour cela que je lui ai aussi assign\u00e9 cinq jours de travail et elle doit absolument \u00eatre commenc\u00e9e apr\u00e8s la lecture.","title":"PT3 Programmation, stockage et mod\u00e8le (5)"},{"location":"index.html#pt4-programmation-vue-de-lapp-5","text":"Cette partie peut \u00eatre horrible comme tr\u00e8s facile, cela d\u00e9pend compl\u00e8tement de la qualit\u00e9 du travail avant. Si le mod\u00e8le est parfait et que les donn\u00e9es sont int\u00e8gres, cela devrait \u00eatre plut\u00f4t simple de les afficher de mani\u00e8re int\u00e9ressante. Cependant, cette partie d\u00e9bordera s\u00fbrement un peu, car tout le temps gagn\u00e9 avec de bonnes donn\u00e9es sera utilis\u00e9 pour tenter de faire de la pr\u00e9diction. Pour ces raisons, je lui ai assign\u00e9 \u00e9galement cinq jours de travail et elle doit absolument \u00eatre faite apr\u00e8s le mod\u00e8le.","title":"PT4 Programmation Vue de l'APP (5)"},{"location":"index.html#pt5-programmation-mise-en-commun-3","text":"Cette t\u00e2che est aussi un petit peu sp\u00e9ciale, car elle regroupe plusieurs choses. En gros, chaque partie de programmation sera s\u00fbrement assez ind\u00e9pendante et il faudra \u00e0 un moment faire un seul projet C# qui contient tout. Il est difficile d'estimer \u00e0 quel point cela va \u00eatre compliqu\u00e9 alors, j'ai \u00e9t\u00e9 conservateur et j'ai mis trois jours.","title":"PT5 Programmation mise en commun (3)"},{"location":"index.html#tt","text":"Cette rubrique contient les t\u00e2ches qui sont uniquement des tests. La plupart des t\u00e2ches de programmations contiennent d\u00e9j\u00e0 des tests, mais certaines demandent une attention particuli\u00e8re.","title":"TT"},{"location":"index.html#tt1-tests-ocr-2","text":"Cette t\u00e2che est une des t\u00e2ches les plus importantes. Son but est de faire un protocole de tests complet qui permette de comparer les diff\u00e9rents algorithmes de reconnaissance de texte. Je l'ai mise apr\u00e8s la reconnaissance, mais m\u00eame maintenant en \u00e9crivant ces lignes, je me dis que dans le planning effectif, elle sera faite pendant la t\u00e2che de programmation. En effet, comment savoir si mon tout nouvel algorithme est r\u00e9ellement mieux que le pr\u00e9c\u00e9dent. Je pr\u00e9vois deux jours, car je pense que faire le dataset va prendre beaucoup de temps, il faut pr\u00e9voir un certain nombre d'images et de texte qui pourront ensuite \u00eatre donn\u00e9es sous forme de tests. C'est certes une t\u00e2che de test, mais c'est aussi de la programmation.","title":"TT1 Tests OCR (2)"},{"location":"index.html#tt2-tests-finaux-2","text":"Cette t\u00e2che de tests est un peu vague, elle regroupe les diff\u00e9rents tests pour v\u00e9rifier que les donn\u00e9es sont bien affich\u00e9es correctement. Ce qui serait cool si j'ai du temps en fin de travail de dipl\u00f4me serait de faire un syst\u00e8me de test qui permet d'entrainer le programme \u00e0 mieux reconnaitre certaines choses comme des arr\u00eats aux stands si on lui donne les trois derni\u00e8res ann\u00e9es de grands Prix. J'ai mis une dur\u00e9e arbitraire de deux jours, mais je ne sais pas vraiment combien de temps cela va vraiment durer. Elle est \u00e9videmment \u00e0 effectuer une fois que tout est \u00e0 peu pr\u00e8s termin\u00e9.","title":"TT2 Tests finaux (2)"},{"location":"index.html#planning-effectif-et-differences","text":"[A remplir dans les derni\u00e8res semaines du travail de dipl\u00f4me]","title":"Planning effectif et diff\u00e9rences"},{"location":"index.html#analyse-fonctionnelle","text":"[A remplir au fur et \u00e0 mesure dans la seconde moiti\u00e9 du travail de dipl\u00f4me]","title":"Analyse fonctionnelle"},{"location":"index.html#analyse-organique","text":"[A remplir au fur et \u00e0 mesure que les features sont d\u00e9velopp\u00e9es]","title":"Analyse Organique"},{"location":"index.html#recuperation-des-images","text":"Voici la premi\u00e8re grande \u00e9tape du projet. Pour rappel, Amazon h\u00e9berge directement le site de la F1TV et poss\u00e8de les droits sur les donn\u00e9es de la F1. C'est sous le nom de AWS (le service d'h\u00e9bergement d'Amazon) que la firme apparait en tant que sponsor. On peut voir ce nom appara\u00eetre assez souvent quand on regarde un Grand Prix car comme ils ont la main-mise sur les donn\u00e9es ils peuvent ins\u00e8rer des bandeaux d'informations sur le flux public sur ce qu'il se passe voir m\u00eame faire des pr\u00e9dictions (Bien qu'un peu bancales) \"AWS example 1\" Ce service s'appelle F1 Insights (Oui c'est un meilleur nom de projet que F1 Companion mais bon) et c'est, je pense, la raison pour laquelle on ne voit aucune API publique qui permette de correctement se renseigner en don\u00e9es en direct pendant un Grand Prix. Ils ont du d\u00e9gotter un juteux contrat pour s'occuper de toute l'infrastructure digitale de la F1 (du moins publique) en \u00e9change d'une exclusivit\u00e9 totale sur certaines choses comme les Data \"AWS example 2\" Evidemment je ne fais que conjecturer et ce que j'ai dit n'est pas \u00e0 prendre au pied de la lettre mais c'est une explication possible je pense de pourquoi il est si difficile de trouver des donn\u00e9es sur la F1 facilement en temps r\u00e9el. Il existe bien quelques API un peu bancales publiques, mais le probl\u00e8me c'est qu'elles ne sont vraiment pas suffisante et je ne peux pas leur faire confiance quand je commente. Ce qu'il m'aurait fallut c'est une API publique et officielle qui me permette d'\u00eatre sur que les donn\u00e9es sont les bonnes et qu'elles arrivent le plus vite possible. On pourrait croire que c'est impossible car cela n'existe pas comme je l'ai dit MAIS ! Ce n'est pas compl\u00eatement vrai. En effet depuis que je poss\u00e8de un abonnement \u00e0 la F1TV, il existe une source d'informations tr\u00e8s pr\u00e9cieuse qui m'aide \u00e9norm\u00e9ment dans mon quotidien de commentateur de Formule 1. La \"DATA CHANNEL\". La Data Channel est une page de la F1TV qui permet, pour chaque Grand Prix, de visualiser, sous la forme d'un flux vid\u00e9o, diff\u00e9rentes informations capitales sur la course. \"Data channel example\" Le probl\u00e8me, c'est que comme je viens de le dire, ces donn\u00e9es ne sont pas accessibles comme un tableau HTML ou un flux RSS ou un tableau JSON. C'est un flux vid\u00e9o. Il faut savoir qu'entretenir une diffusion de flux vid\u00e9o en 1080P pendant deux heures accessible par des milliers d'abonn\u00e9s est EXTR\u00caMENT cher surtout quand on le compare \u00e0 simplement afficher les donn\u00e9es dans un tableau. Ce qui veut dire que ce choix est d\u00e9lib\u00e9r\u00e9 et a un sens au niveau \u00e9conomique. Je pense donc que c'est justement pour \u00e9viter que des petits malins puissent simplement venir scraper l'int\u00e9gralit\u00e9 des donn\u00e9es qu'ils proposent et fasse sa propre API. (C'est d'ailleurs un des sites avec la meilleure protection anti bot du monde) MAIS ce n'est pas par ce que les donn\u00e9es ne sont pas facile \u00e0 avoir qu'elles sont IMPOSSIBLE \u00e0 avoir. Et c'est la que ce projet entre en jeu. Mais pour d\u00e9coder les donn\u00e9es d'une image il faut dabord ... (roulement de tambours) ... Avoir des images ! Et c'est la que vient se glisser cette partie du projet.","title":"R\u00e9cup\u00e9ration des images"},{"location":"index.html#comment-faire","text":"Le but de ce segment est de se concentrer sur la r\u00e9cup\u00e8ration et la mise \u00e0 disposition pour le reste du programme, des images en direct de la F1TV dans la meilleure qualit\u00e9 possible et dans les meilleurs d\u00e9lais. Pour ce faire il y a plusieurs solutions : Reverse engeneer la F1TV pour acc\u00e8der directement au flux sans passer par la plateforme internet et pouvoir prendres images \u00e0 volont\u00e9. Avoir tout simplement une page de la F1TV ouverte sur un \u00e9cran et prendres des screenshots \u00e0 intervals r\u00e9guliers. Simuler un navigateur internet sans qu'il soit affich\u00e9 et le contr\u00f4ler automatiquement pour qu'il prenne des captures. La premi\u00e8re option aurait \u00e9t\u00e9 la plus \u00e9l\u00e9gante mais lors d'un POC que je tentais de r\u00e9aliser je me suis rendu compte que cela serait un peu trop compliqu\u00e9 et long \u00e0 faire. Sans compter le fait que les rediffusions de Grand Prix ne sont pas g\u00e8r\u00e9es de la m\u00eame mani\u00e8re que les diffusions en live. Et que pour faire des Tests en live il faudrait attendre \u00e0 chaque fois un weekend de Grand Prix et le faire en plus du commentaire que je dois produire. Pour toutes ces raisons et bien d'autres je l'ai rang\u00e9e dans la case \"Trop dur, Trop chiant, S\u00fbrement ill\u00e9gal\" (Oui je sais c'est une cat\u00e9gorie bien sp\u00e9cifique mais c'est ma documentation je fais ce que je veux) La troisi\u00e8me option aurait \u00e9t\u00e9 la plus simple (et moins dr\u00f4le) et je suis presque s\u00fbr que je peux impl\u00e9menter cette derni\u00e8re en moins d'une apr\u00e8s-midi. Sauf qu'elle apporte de gros soucis. On ne peux pas garantir l'int\u00e9grit\u00e9 et la continuit\u00e9 des donn\u00e9es si l'utilisateur avance ou fait pause m\u00eame par simple inadvertance. La moindre fen\u00eatre qui s'afficherait devant ruinerait toute la reconnaissance de caract\u00e8res. On ne peut pas contr\u00f4ler la qualit\u00e9 du flux et on est oblig\u00e9 de faire confiance en l'utilisateur On ne peut pas vraiment automatiser quoi que ce soit niveau tests ou m\u00eame pour faire du scrapping auto pour remplir une base de donn\u00e9e. Et finalement le pire inconv\u00e9nient : C'EST NUL ! Je ne pourrais jamais utiliser un projet qui fonctionne de cette facon, je ne peux pas me permettre d'avoir un \u00e9cran inutilisable quand je commente et auquel je dois constamment faire attention pour ne pas perturber la reconnaissance. Pour moi cette option aurait \u00e9t\u00e9 celle \u00e0 choisir en cas d'extr\u00eame urgence et en dernier recours car le projet deviendrait inutile. J'ai donc d\u00e9cid\u00e9 de m'occuper de la seconde option : Simuler un navigateur. Cette option bien que complexe et difficile \u00e0 impl\u00e9menter propose une solution \u00e0 tous les probl\u00eame et permet une r\u00e9cup\u00e8ration quasi sans compromis.","title":"Comment faire ?"},{"location":"index.html#simuler-un-navigateur","text":"Simuler un navigateur internet n'est pas forc\u00e9ment tr\u00e8s difficile. Chromium par exemple offre une panoplie d'outils natifs et \u00e9norm\u00e9ment de librairies existent permettant de facilement et en quelques lignes simuler un Google Chrome et le contr\u00f4ler sans afficher son UI. \"Chromium logo\" {: style=\"height:150px;width:150px\"} Cependant. La F1TV n'utilise pas simplement un player HTML5 basique. Elle utilise un service de streaming BitMovin qui permet de fournir un stream de bonne qualit\u00e9 et surtout qui impl\u00e9mente les DRM (Digital Right Management) Cela veut dire que quand on ouvre un flux de la F1TV sur chrome et que l'on essaie de prendre une capture d'\u00e9cran, le player se met en noir et ne permet pas de voir quoi que ce soit (Certaines version de Chrome le permettent pendant quelques semaines avant de bloquer \u00e0 nouveau). Ce qui dans notre cas est un immense probl\u00e8me. Mais Firefox ne nous bloque pas de cette facon et il est donc assez facile de passer outre. L'explication sans trop rentrer dans les d\u00e9tails est la suivante : Dans chrome, le player par d\u00e9faut utilise une technologie appell\u00e9e \"PCP\" ou \"Protected Content Playback\" qui leur permet de bloquer au moins une partie des techniques de r\u00e9cup\u00e8ration du flux vid\u00e9o et audio. Cependant Firefox de pas sa nature Open Source utilise \"OpenH264\" pour lire ces m\u00eames flux soumis \u00e0 des DRM et OpenH264 n'impl\u00e9mente pas les m\u00eames restrictions. Sauf que Firefox n'est pas aussi facilement \u00e9mul\u00e9 que chrome et cela r\u00e9duit notre choix de librairies \u00e0 ... Une seule... Qui est Selenium. (Il existe aussi Pupetteer C# mais j'ai rencontr\u00e9 \u00e9norm\u00e9ment de soucis avec cette derni\u00e8re d\u00e8s que je voulais lancer une vid\u00e9o) \"Firefox Developper logo\" {: style=\"height:150px;width:150px\"} Mais m\u00eame si la documentation est plut\u00f4t maigre parfois, c'est une bonne librairie qui permet de tr\u00e8s bien contr\u00f4ler une instance de chrome ou de Firefox.","title":"Simuler un navigateur ?"},{"location":"index.html#controler-le-navigateur","text":"Maintenant que l'on sait quel navigateur simuler et avec quelle technologie, on peut passer \u00e0 la r\u00e9alisation. Ce qu'il y a de bien avec Selenium, c'est qu'on a un certain nombre de commandes tr\u00e8s haut niveau qui nous permettent de contr\u00f4ler un navigateur de mani\u00e8re plut\u00f4t pr\u00e9cise. Je vais d\u00e9crire ici la proc\u00e9dure habituelle utilis\u00e9e sous une forme de recette de cuisine pour que l'on puisse facilement comprendre ce qu'il se passe. Durant cette explication je vais parler \u00e0 un moment de Cookies, ne vous en faites pas c'est le sous chapitre suivant qui va vous en parler. Recette de cuisine pour r\u00e9cup\u00e8rer des images de la F1TV : D\u00e9marrer une instance de navigateur avec les bons arguments Ajouter les bons param\u00eatres pour ne pas se faire flag comme un bot Naviguer sur la page de la F1TV Ajouter les cookies de connexion pour avoir acc\u00e8s au contenu de la page Naviguer sur la page du Grand Prix demand\u00e9 Attendre un peu que la page se charge Cliquer sur l'invite de cookies Attendre cinq secondes le temps que la page se reload Cliquer sur le bouton qui permet de passer du feed live \u00e0 la DATA CHANNEL Appuyer sur Espace pour faire apparaitre le bouton d'acc\u00e8s au param\u00eatres Cliquer sur le menu d\u00e9roulant des r\u00e9solution Trouver l'option 1080P et la selectionner Cliquer sur le bouton qui met la vid\u00e9o en plein \u00e9cran Prendre de screenshots \u00e0 intervales r\u00e9guliers Pour faire toutes ces actions on doit r\u00e9cup\u00e8rer les \u00e9l\u00e9ments selon leur ID ou leur classe. Voici un exemple qui r\u00e9cup\u00e8re le bouton de plein \u00e9cran et qui clique dessus : IWebElement fullScreenButton = Driver.FindElement(By.ClassName(\"bmpui-ui-fullscreentogglebutton\")); fullScreenButton.Click(); Ca peut para\u00eetre plut\u00f4t simple dit comme ca et quand tout fonctionne ca l'est mais la difficult\u00e9 vient du fait que \u00e0 peu pr\u00e8s nimporte laquelle de ces \u00e9tapes peut rater et qu'il faut donc faire un bon syst\u00e8me de gestion d'erreurs qui puisse aider l'utilisateur en cas de probl\u00e8me. Il faut dire aussi que les sites ne sont pas forc\u00e9ment tr\u00e8s content de voir des bots passer car cela peut \u00eatre un risque de DDOS et de Scraping (Comme moi) et donc ils mettent en place des syst\u00e8mes pour nous emp\u00eacher de faire ce que l'on veut On peut utiliser diff\u00e9rntes techniques pour passer outre ces restrictions comme : Changer son UserAgent Changer sa r\u00e9solution Ne pas avoir des patterns trop pr\u00e9visibles Avoir un historique Ne pas cliquer pile sur le milieu des boutons Ne pas cliquer trop vite Passer par un proxy pour ne pas se faire flag Utiliser des librairies plus discr\u00e8tes J'ai eu l'occasion de tester toutes ces methodes pour tenter de passer derri\u00e8re les radars de la F1TV et visiblement j'ai r\u00e9ussi pour les pages principales mais pas pour les pages de Login. Il faut savoir que la bataille entre bots et propri\u00e9taires de sites est un grand jeu du chat et de la souris et que les plateformes innovent constamment leur s\u00e9curit\u00e9. Et il se trouve que la partie login de la F1TV est heberg\u00e9e autre part que le reste du site chez Amazon et que elle poss\u00e8de les meilleures s\u00e9curit\u00e9s que j'aie pu voir. Aucunes des methodes que j'ai cit\u00e9es et d'autres encore que j'ai essay\u00e9 n'ont r\u00e9ussi \u00e0 fourvoyer le syst\u00e8me. J'ai donc \u00e9t\u00e9 oblig\u00e9 de faire appel \u00e0 la connexion par Cookies pour pouvoir acc\u00e8der au reste du site internet.","title":"Contr\u00f4ler le navigateur"},{"location":"index.html#recuperer-les-cookies","text":"Alors, on va mettre de c\u00f4t\u00e9 toutes les questions de s\u00e9curit\u00e9 et de violation de la vie priv\u00e9e et de protection des donn\u00e9es des utilisateurs pour ce chapitre. Car pour faire simple, je siphonne TOUS les cookies de la persone qui utilise mon app. [FINIR CETTE EXPLICATIOn]","title":"R\u00e9cup\u00e8rer les cookies ?"},{"location":"index.html#calibration","text":"[AJOUTER EXPLICATION]","title":"Calibration"},{"location":"index.html#ocr","text":"Ici je vais parler de la seconde partie du projet qui parle du processus de reconnaissance de data sur une image du feed DATA de la F1TV. C'est je pense la partie qui a demand\u00e9 le plus tests et de refactor. Toute la partie OCR a \u00e9t\u00e9 d\u00e9velopp\u00e9e dans un projet \u00e0 part avant d'\u00eatre int\u00e9gr\u00e9e dans le projet final. Il faut savoir que la reconnaissance est diff\u00e9rente celon ce que l'on cherche. Je vais donc d\u00e9composer cette partie du document en sous rubriques selon les donn\u00e9es recherch\u00e9es. Mais avant ca je dois expliquer certains concepts qui seront importants.","title":"OCR"},{"location":"index.html#fonctionnement-general","text":"Voici un screenshot de la page DATA de la F1TV que le programme va recevoir : \"Screen F1TV\" Si on regarde de loin on peut se dire que la structure est plut\u00f4t simple mais c'est loin d'\u00eatre le cas. On peut y voir au moins 4 zones contenant de l'information dans un format diff\u00e9rent. \"Main zones\" Dans l'exemple ci dessus on peut voir 3 zones mais on aurait \u00e9galement pu comprendre la zone de position des pilotes autour du circuit pour faire 4. Ces 4 zones sont tr\u00e8s diff\u00e9rentes et contiennent d'autres informations. Pour ce travail de dipl\u00f4me je ne m'occupe que de la zone principale. Mais je pense que le titre et les infos de circuit ne prendrait pas tant de temps que ca \u00e0 impl\u00e9menter. J'ai utilis\u00e9 le mot \"Zone\" plus haut et ca n'est pas juste un mot utilis\u00e9 au hasard. C'est le nom de l'objet que j'utilise pour les repr\u00e9senter dans mon programme. Mais comme c'est important de bien comprendre ce concept avant de continuer je vais vous l'expliquer. ZONE : L'objet \"Zone\" parent est un objet qui est une zone d'image. Je m'explique, le but d'une zone est d'\u00eatre un morceau d'une image plus grande. Le but d'une Zone est de contenir une liste de plus petites Zones ou bien une liste de \"Window\" (j'explique ce que c'est juste apr\u00e8s). Elle contient la portion d'image qui la concerne et ses propres dimensions. Le parent zone ne pr\u00e9voit que de pouvoir ajouter ou supprimer des \u00e9l\u00e9ments des listes de zones ou de windows ainsi qu'une methode qui permet d'aller chercher toutes informations des livres qu'elle contient. L'int\u00e9r\u00eat d'une zone est de pouvoir compartimenter une image dans des parties int\u00e9ressantes au niveau de la reconnaissance mais pas de traiter d'information. WINDOW : L'objet \"Window\" est un objet qui peut ressembler beaucoup \u00e0 l'objet \"Zone\". En effet elle aussi est une partie d'une image plus grande et contient ses dimensions, mais elle se distingue en deux points importants. Elle ne contient pas d'autres Zones ou Windows Elle peut retourner les informations \u00e9crites sur son image. Toutes les Window qui h\u00e9ritent du parent Window peuvent impl\u00e9menter une methode qui permet de renvoyer ce qui peut \u00eatre d\u00e9cod\u00e9 sur son image. Les enfants peuvent aussi aller piocher dans les nombresues methodes de r\u00e9cup\u00e8ration de donn\u00e9es contenues dans le parent Window. Mieux vaut r\u00e9utiliser le plus possible que de r\u00e9inventer la roue pour chaque Window. Une analogie un peu bancale pourrait se pr\u00e9senter comme la suivante : La zone est une armoire ou une bibliot\u00e8que. Si c'est une zone qui contient d'autres zones c'est une bibliot\u00e8que et chacune de ces sous-zones sont des armoires. Leur unique but est de contenir de mani\u00e8re ordonn\u00e9e des objets qui eux contiennent de l'information. Les livres ici sont les Windows. Ils contiennet de l'information et sont stock\u00e9s dans des armoires et on y acc\u00e8de en allant dans la bonne bibliot\u00e8que et en allant dans la bonne armoire. Derni\u00e8res choses pour comprendre le diagramme: Il existe une Main Zone qui est une des 4 grandes zones dont je parlais dans la d\u00e9composition de l'image. Il existe aussi des \"Driver Zone\" qui sont de plus petites zones contenues dans la Main Zone qui et qui ne contiennent que les informations d'un pilote. L'objet Window n'est quasi jamais utilis\u00e9, c'est presque tout le temps des enfants de Window plus sp\u00e9cifiques qui sont utilis\u00e9s, le but est que chaque type d'information sur l'image aie son type de window. Voila donc un petit diagramme qui montre le d\u00e9coupage du programme : \"Diagramme zones\" Pour visualiser encore un peu mieux comment ce d\u00e9coupage prend forme voici ce que chaque zone et Window contient. Main Zone : \"Main zone\" Driver Zone : \"Driver zone\" Driver Position Window : \"Driver position Window\" Driver name Window : \"Driver name window\" Driver LapTime Window : \"Driver Laptime window\" Driver Tyre Window : \"Driver tyre window\" Il existe d'autres types de Window mais ce sont les principaux. On se rend assez facilement compte que chacunes de ces windows va avoir besoin d'un traitement sp\u00e9cifique car la mani\u00e8re de reconnaitre le pneu utilis\u00e9 et le temps au tour ne peut pas \u00eatre la m\u00eame. Pour r\u00e9sumer, on a un programme qui prend en entr\u00e9e un fichier de configuration, qui prend des images de la F1TV et les d\u00e9coupe dans des ZONES qui elles m\u00eame sont d\u00e9coup\u00e9es en WINDOWS pour qu'on puisse plus facilement les d\u00e9coder. Maintenant qu'on a une liste de diff\u00e9rent types de zones on peut commencer \u00e0 chercher ce qu'il y a marqu\u00e9 dessus. Pour cela il faut dabord comprendre un petit peu comment l'OCR fonctionne et comment des libraries comme Tesseract fonctionnent pour donner du texte en partant d'une image. Pour faire tr\u00e8s simple, nous avons un mod\u00e8le qui est entrain\u00e9. C'est \u00e0 dire que on donne \u00e0 un programme un tr\u00e8s grand nombre de mots ou de lettres en lui disant ce que contiennent chaques images. Ensuite le programme va cr\u00e9er des matrices de convolutions pour chaque lettre avec comme objectif de detecter les points communs entre les lettres pour cr\u00e9er un alpphabet. Par exemple la matric de la lettre 'H' donnerait un poids important \u00e0 des lignes verticales connect\u00e9es par une ligne centrale. Et si on fournis assez de donn\u00e9es de bonne qualit\u00e9 au mod\u00e8le, les matrices peuvent \u00eatre tr\u00e8s efficace \u00e0 detecter si une lettre est un H ou un M. Il y a pleins d'autres methodes comme l'utilisation d'un dictionnaire de mots de la langue pour permettre la reconnaissance de mots m\u00eame si une lettre au milieu n'est pas comprise ou en ajoutant d'autres informations sur le contexte mais ca ne nous int\u00e9resse pas ici. C'est important de comprendre comment cette reconnaissance de caract\u00e8res avec des matrices fonctionne car cela va nous aider \u00e0 pr\u00e9parer nos donn\u00e9es pour lui rendre la vie facile et augmenter la pr\u00e9cision de nos r\u00e9sultats.","title":"Fonctionnement g\u00e9n\u00e9ral"},{"location":"index.html#filtres-et-traitement","text":"On peut essayer de donner toutes nos images directement \u00e0 Tesseract pour qu'il reconnaisse tout le texte qu'il y voit mais on risque de se retrouver avec des r\u00e9sultats au mieux inconsistents. Dans notre cas, le soucis est que les chiffres et lettres sont beaucoup trop petits. Ils ne font parfoisd que 10 pixels de haut et cela fait que il n'est pas forc\u00e9ment facile de toujours les diff\u00e9rencier. De plus, comme ils sont petits, les art\u00e9facts d'aliasing sont assez violents et peuvent grandement d\u00e9former une lettre ou un chiffre. Exemple : Prenons le chiffre 9. Dans l'image il peut \u00eatre repr\u00e9sent\u00e9 de cette mani\u00e8re : \"Bad9Exemple\" On peut voir qu'il est flou, pour nous cela ne pose pas de probl\u00e8me et je pense que \u00e0 peu pr\u00e8s nimporte qui peut dire que c'est un 9. Cependant comme les contours sont flous et m\u00eame si on essaie de retirer le background : \"Aliased 9\" On voit que le 9 n'est pas clairement d\u00e9finit. En effet on pourrait le comprendre comme : \"First contour\" Ou comme : \"Second contour\" Voire m\u00eame simplement comme : \"Big contour\" Et on se rend bien compte que les performances de detection ne sont pas les m\u00eames dans ces trois cas. Il faut donc faire un certain post traitement des images pour supprimer les \u00e9l\u00e9ments parasites, les couleurs, et augmenter la visibilit\u00e9 des contours importants. Mais chaque type de donn\u00e9e va avoir des methodes de post traitement diff\u00e9rents. Donc voici les diff\u00e9rents types de reconnaissance et leur post traitements :","title":"Filtres et traitement"},{"location":"index.html#texte","text":"Alors ce type de reconnaissance est utilis\u00e9 par la WINDOW du nom de pilote et de la position du pilote. C'est je pense la plus simple de toutes car Tesseract est particuli\u00e8rement bien entrain\u00e9 pour. Cette reconnaissance concerne donc des lettres qui font des mots ou des noms. Voici un exemple de la WINDOW nom de pilote en entr\u00e9e : \"Exemple raw\" Ce texte peut paraitre bon, cependant quand on le lance dans Tesseract, il ne va pas toujours donner un r\u00e9sultat parfait. Il faut aussi savoir qu'il y a des noms pas mal plus p\u00e9nibles que Tesseract a plus de mal \u00e0 reconnaitres, soit \u00e0 cause des lettres utilis\u00e9es, soit car le nom est un nom d'une autre r\u00e9gion et qui ne veut rien dire en anglais ce qui emp\u00eache l'utilisation de dictionnaire (Ex : Tsunoda est un nom japonais et parfois il est difficile pour Tesseract de le reconnaitre car si une lettre pose probl\u00eame il ne peut pas trouver de contexte qui puisse l'aider). Donc pour le rendre plus facilement lisible et augmenter les chances que toutes les lettres soient d\u00e9couvertes, voici les \u00e9tapes que j'ai mis en place. 1 : J'inverse les couleurs. Je me suis rendu compte que il \u00e9tait souvent plus facile de trouver un noir sur blanc que blanc sur noir. Je ne suis pas sur que cette \u00e9tape soit capitale cependant. \"Texte invers\u00e9\" 2 : Je fais un Treshhold de 165 car avec moins le texte parfois prend trop du background et avec plus les lettres sont trop fines. \"Treshold\" 3 : Je fais un Resize de l'image pour avoir une meilleure r\u00e9solution et permettre une meilleure d\u00e9tection. J'augmente la hauteur et la largeur par un facteur 2. J'ai trouv\u00e9 cette valeur suffisante et aller plus haut consomme beaucoup de ressources. \"Resize\" 4: Je fais une tr\u00e8s rapide Dilatation du texte pour retirer le flou amen\u00e9 par la methode de Resize. Je n'utilise qu'une valeur de 1 car je ne veux pas trop changer comment le texte est model\u00e9 je veux juste retirer le flou. \"Dilatation\" Explication des methodes pr\u00e9cises plus bas Voila pour ce qui est du post processing. Je ne dis pas que ce sont les meilleurs param\u00eatres possibles mais dans mes tests ce sont ceux qui ont le mieux march\u00e9s. C'est aussi les premi\u00e8res methodes que j'ai pu d\u00e9velopper alors forc\u00e9ment elles n'ont pas le niveau de d\u00e9tails de certaines autres. Mais comme m\u00eame avec ce traitement il n'est pas rare que je me retrouve avec une ou deux lettres pas justes, il faut un moyen d'\u00eatre s\u00fbr que c'est le bon nom qui est trouv\u00e9. Ce qu'il y a de pratique avec les noms de pilotes c'est que on sait d\u00e9ja comment ils s'appellent avant le Grand Prix. En effet dans le fichier de configuration de la reconnaissance, il y a une liste de noms de pilotes. Cela veut dire que au lieu de chercher \u00e0 trouver parfaitement les bonnes lettres, on peut simplement essayer de trouver quel nom de pilote ressemble le plus au nom trouv\u00e9 sur l'image. Pour ce faire j'ai utilis\u00e9 une methode appel\u00e9e la distance de Levenshtein. Pour faire simple c'est une methode qui va calculer les distances de lettres pour determiner entre des strings laquelle ressemble le plus \u00e0 une autre. Pour r\u00e9sumer le fonctionnement dans lordre : On prend l'image on la traite On envoie l'image trait\u00e9e \u00e0 Tesseract On trouve quel nom de pilote ressemble le plus \u00e0 ce r\u00e9sultat On renvoie le nom du pilote","title":"Texte"},{"location":"index.html#chiffres","text":"Cette methode en r\u00e9alit\u00e9 utilise simplement la m\u00eame methode que celle qui va r\u00e9cup\u00e8rer le texte sur une image. Cependant, la, on envoie \u00e0 Tesseract l'information qu'il ne peut trouver que des chiffres sur l'image ce qui lui permet d'\u00eatre beaucoup plus pr\u00e9cis et de ne pas confondre un 9 avec un P ou un 11 avec un H PAR EXEMPLE (non pas que ca me soit arriv\u00e9 tr\u00e8s r\u00e9guli\u00e8rement et que ca me soit rest\u00e9 dans la gorge \u00e9videmment) L'avantage c'est que cette methode ne demande m\u00eame pas de traitement de la donn\u00e9e en sortie de Tesseract. On \u00e9sp\u00e8re simplement que le post traitement aura suffit. TEMPS : Cette methode regroupe la d\u00e9tection de temps au tour. Il y a trois grands types de WINDOW qui sont concern\u00e9es : La WINDOW du temps au tour La WINDOW du retard sur le leader La WINDOW des secteurs La grande diff\u00e9rence ce sont les ordres de grandeur. Les temps au tour sont en g\u00e9n\u00e9ral entre 50 secondes et 2 minutes. Tandis que les secteurs sont entre 20 et 30 secondes alors que le retard sur le leader peut-\u00eatre de plusieurs minutes. Cependant, tous ces temps poss\u00e8dent le m\u00eame type de post-traitement avant d'\u00eatre envoy\u00e9s \u00e0 Tesseract. Voici un exemple de temps au tour avant toute transformation : \"Lap time\" On peut avoir l'impression que ce texte est tout \u00e0 fait lisible et facile \u00e0 d\u00e9coder surtout quand on le voit de loin comme ca. Cependant, il faut imaginer que ces chiffres font 13 pixels de haut en comptant le flou et comme expliqu\u00e9 plus haut ce flou dans ces echelles est terrible. \"Lap time\" Si on donne cette image \u00e0 Tesseract, les '3' deviennent des '9', des '9' deviennent des '8', des '2' deviennent eux aussi des '9', le tout parfois inversement et de mani\u00e8re compl\u00eatement impr\u00e9visible. Ca n'est simplement pas utilisable. Cette partie est un peu plus complexe car si la detection n'est pas fiable les chiffres sont simplement inutilisables. Si \u00e0 tout moment un temps au tour de 1:39.106 devient 1:32.108 c'est juste pas possible. Voici donc les \u00e9tapes de post-traitement que j'ai mis en place pour leur d\u00e9tection : 1: J'applique un Treshold de 185 pour enlever les ambiguit\u00e9s d'alisaising et avoir une image en noir et blanc claire. La valeur de 185 est assez \u00e9lev\u00e9e car le but est de vraiment garder uniquement les contours. Comme les chiffres se ressemlent beaucoup plu que les lettres, il faut tenter le plus possible de conserver leur formes sp\u00e9cifiques. Je me suis rendu compte que cette valeur \u00e9tait une de celles qui marchent le mieux. \"Treshold\" 2: J'applique un Resize de 2 pour augmenter la r\u00e9solution des chiffres et permettre une meilleure d\u00e9tection. Le but est d'avoir plus de pixels et donc de permettre \u00e0 Tesseract de mieux utiliser ses matrices de convolution. \"Resize\" 3: Comme le Resize am\u00e8ne du flou, j'utilise une methode de Dilatation qui me permet de retirer ce flou et de remplir un peu plus certaines parties qui ont \u00e9t\u00e9 un peu laiss\u00e9e par le Resize*; \"Dilatation\" 4: Contrairement aux mots plus haut, la rondeur ajout\u00e9e par la dilatation n'est pas vraiment d\u00e9sir\u00e9e. En effet, elle peut rendre confuse certains chiffres et emp\u00eacher Tesseract de bien trouver le chiffre. Alors j'applique une Erosion qui me permet de contrecarrer en partie les rondeurs ajout\u00e9es par la dilatation et retrouver des chiffres bien form\u00e9es. Pour l' Erosion et la Dilatation j'ai utilis\u00e9 une valeur de 1 car je ne voulais pas trop changer les chiffres. \"Erode\" Explication des methodes pr\u00e9cises plus bas Et avec ce post processing on retrouve de plut\u00f4ts bon r\u00e9sultats qui demandent peu de traitement. Le traitement d\u00e9pend du type de WINDOW cependant. Pour les secteurs on indique \u00e0 Tesseract que les caract\u00e8res autoris\u00e9s sont : \"0123456789.\" Pour les temps au tour on autorise plut\u00f4t \"0123456789.:\" Et pour les \u00e9carts on autorise \"0123456789.+\" Ensuite on r\u00e9cup\u00e8re une liste de chiffres qui'il va falloir transformer en milisecondes pour faciliter le stockage et l'envoi. Le programme nettoie un peu la chaine avant de la convertir. Par exemple parfois le ':' de 1:34.456 est compris comme un '1' ou un '2' et il faut faire attention \u00e0 detecter quand ca arriver. Je passe les d\u00e9tails du reste du nettoyage car c'est vraiment du cas par cas mais quand on a finit de nettoyer la chaine on peut transformer les chaines de minutes secondes et milisecondes en un total de milisecondes. Pour r\u00e9sumer le fonctionnement dans l'ordre : On prend l'image et on lui applique une s\u00e9rie de filtres On envoie l'image filtr\u00e9e \u00e0 Tesseract On nettoie le r\u00e9sultat Tesseract pour compenser certains biais On convertis le r\u00e9sultat en milisecondes","title":"Chiffres"},{"location":"index.html#pneus","text":"La on arrive sur la partie la plus p\u00e9nible. Pour comprendre la probl\u00e9matique il faut d'abord faire un petit point sur comment les pneus fonctionnent en Formule 1. Depuis 2019 en Formule 1 nous avons 5 grandes familles de pneus : Les pneus tendres Les pneus medium Les pneus durs Les pneus interm\u00e9diaires Les pneus pluie \"Tyres\" Les trois premiers pneus sont des pneus faits pour piste s\u00e8che, le pneu interm\u00e9diaire pour piste humide et le neu pluie pour la pluie. Chaque pneu a sa dur\u00e9e de vie et son niveau de performance propre mais je ne vais pas rentrer dans le d\u00e9tail ici. Tout ce qu'il faut savoir ce que savoir sur quel pneu chaque pilote est et depuis combien de temps il les chausse est une information tr\u00e8s importante. Chaque pneu a une couleur donn\u00e9e qui permet de les diff\u00e9rencier. Voici un exemple de ce \u00e0 quoi une WINDOW de pneus peut ressembler : \"Exemple 1\" Mais cette zone peut aussi ressembler \u00e0 ca : \"Exemple 2\" Mais aussi \u00e0 ca : \"Exemple 3\" Voire m\u00eame ca : \"Exemple 4\" Je pense que vous pouvez tout de suite comprendre la difficult\u00e9 que repr\u00e9sente la t\u00e2che de r\u00e9cup\u00e8ration de donn\u00e9es \u00e0 partir de cette image. En gros le fonctionnement de cette zone d'information est assez simple. Au fur et \u00e0 mesure que la course avance, le trait fait de m\u00eame. Le chiffre dans le round tout \u00e0 droite indique le nombre de tour que le pilote a pass\u00e9 sur ce pneu. La couleur indique le type de pneu. Si il y a une lettre \u00e0 la place d'un chiffre c'est que c'est le premier tour sur ce pneu. La lettre indique le type de pneu. Et pas besoin de dire que si on essaie simplement de donner l'image \u00e0 Tesseract on ne r\u00e9cup\u00e8re ni les chiffres ni les lettres correctement si ce n'est pas du tout. Il faut donc utiliser une methode qui permette d'isoler le rond le plus \u00e0 droite, lui appliquer un traitement qui permette \u00e0 Tesseract de lire ce qu'il y a marqu\u00e9 et qui puisse determiner quel pneu est en train d'\u00eatre utilis\u00e9. J'ai d\u00e9cid\u00e9 de m'occuper dans un premier temps de trouver ce rond avant d'appliquer les filtres car plus l'image est petite plus les filtres sont rapides. Le programme va tirer un trait depuis le bord droit de la zone, et il va avancer vers la gauche jusqu'\u00e0 trouver un obstacle. Je d\u00e9tecte un obstacle si le pixel sur lequel est mon trait poss\u00e8de une valeur de plus de 0x50 dans le channel R,G ou B. J'ai trouv\u00e9 en faisant des tests que les couleurs de background de la F1TV ne d\u00e9passaient jamais ces valeurs. Ensuite apr\u00e8s avoir trouv\u00e9 le premier obstacle, je r\u00e9cup\u00e8re une zone qui doit englober le cercle. Voici un exemple avec cette image en entr\u00e9e : \"Full zone\" Elle est automatiquement coup\u00e9e de cette facon : \"Cropped zone\" Cela me permet d'isoler uniquement ce qui m'int\u00e9resse ce qui est tr\u00e8s pratique pour Tesseract et pour la detection de couleur. Ensuite avec cette image je peux commencer le processus de reconnaissance. Je commence par faire une moyenne de tous les pixels de l'image en excluant les pixels trop sombres qui font s\u00fbrement partie du background ou du chiffre. Ensuite j'utilise une methode qui calcule la diff\u00e9rence entre la couleur obbtenue et la liste de couleurs possible. Il y a cinq couleurs des pneus possibles : \"#ff0000\" pneu tendre/soft \"Soft tyre color\" \"#f5bf00\" pneu medium \"medium tyre color\" \"#a4a5a8\" pneu dur/hard \"Hard tyre color\" \"#00a42e\" pneu inter \"Inter tyre color\" \"#2760a6\" pneu pluie/wet \"Wet tyre color\" Ce qui est pratique c'est que m\u00eame dans les cas ou il n'y a pas beaucoup de couleur comme celui la : \"Hard tyre but only the letter\" On arrive \u00e0 une couleur moyenne de : \"The average color from the picture above\" Et il est donc assez facile de determiner le type de pneu en question. Attention, les r\u00e9sultats peuvent \u00eatre tr\u00e8s vite d\u00e9rang\u00e9s par la couleur du pneu pr\u00e9c\u00e9dent si le d\u00e9coupage de la fen\u00eatre n'a pas \u00e9t\u00e9 assez pr\u00e9cis. Ensuite il \"suffit\" de lire le chiffre dans le rond et si on arrive pas \u00e0 le lire alors c'est que c'est une lettre et on sait que le nombre de tours est donc de 0. Maintenant vient le moment tr\u00e8s sympatique de la lecture du chiffre. Vous saurez que Tesseract en plus de detester les grandes images et les images avec des couleurs, deteste \u00e9galement les formes dans une image. Donc dans notre cas, le round de couleur autour du chiffre, m\u00eame si il n'est pas complet, il interf\u00e8re avec la reconnaissance et emp\u00eache de bien lire le chiffre. Il faut donc retirer le background et ensuite la couleur. Sauf que comme le chiffre est de la couleur du background, si on retire le background et ensuite la couleur il ne reste plus rien. Il faut donc retirer le background AUTOUR du rond, et ensuite si on retire la couleur il devrait rester le chiffre sur fond blanc. Pour se faire, j'ai tir\u00e9 des traits depuis les bords de l'image jusqu'\u00e0 ce qu'ils rencontrent le rond. Ensuite je retire tous les pixels entre le rond et les bords de l'image ce qui nous donne ceci : \"No outer background\" Ensuite on peu retirer les pixels qui ont une valeur dans un channel RGB plus haute qu'un certain seuil : \"Only digit\" Et la on a ce que l'on veut ! A partir de la c'est les filtres que l'on connait qui sont utilis\u00e9s pour en faire une image plus facile \u00e0 utiliser par Tesseract. 1 : On effectue un Resize de facteur 4 (oui c'est beaucoup mais en m\u00eame temps le chiffre est vraiment petit \u00e0 la base) qui permet d'avoir une image d'une bien meilleure r\u00e9solution. \"Filter 1\" 2: On fait une Dilatation de facteur 1 pour retirer tout le flou de l'image pour aider Tesseract \"Result\" Et on a un chiffre qui est utilisable par Tesseract ! Explication des methodes pr\u00e9cises plus bas Pour r\u00e9sumer : On prend l'image de la zone et on la crop pour ne garder que la partie essentielle On d\u00e9termine le type de pneu avec la couleur moyenne de la zone On retire le background autour de cette zone On retire la couleur qui reste pour ne garder que le chiffre On augmente la r\u00e9solution du chiffre On rend ce chiffre net On envoie l'image trait\u00e9e et filtr\u00e9e \u00e0 Tesseract On d\u00e9termine le nombre de tours que le pilote a fait avec ses pneus avec le r\u00e9sultat de Tesseract","title":"Pneus"},{"location":"index.html#drs","text":"Bon ca c'\u00e9tait plut\u00f4t simple j'ai simplement v\u00e9rifi\u00e9 si la moyenne de vert d\u00e9passait une certaine valeur et puis voila.","title":"DRS"},{"location":"index.html#filtres-et-methodes-sur-les-images","text":"Dans ce projet on a du utiliser diff\u00e9rentes methodes d'\u00e9dition d'image que ce soit sous forme de filtres ou de modification de l'image directement. Voici un sommaire des methodes utilis\u00e9es et comment elles fonctionnent. Tresholding Cette methode sert \u00e0 passer d'une image en couleurs \u00e0 une image binaire noir blanc. C'est une \u00e9tape tr\u00e8s importante pour l'OCR car elle permet (si bien faite) d'isoler du texte de son background. Un exemple ici : \"Exemple treshold\" Le fonctionnement est assez simple mais il peut \u00eatre fait de diff\u00e9rentes mani\u00e8res mais dans mon cas voici comment l'algorythme fonctionne sachant qu'il demande en entr\u00e9e la Bitmap que l'on veut modifier ainsi que la valeur de Treshold : On parcours chaque pixel de l'image On convertir la couleur du pixel en une valeur de gris pour avoir la m\u00eame valeur en R,G et B (Formule utilis\u00e9e : grey = R x 0.3 + G x 0.59 + B x 0.11) Si le r\u00e9sultat de la valeur de gris est au dessus de la valeur de treshold, le pixel est pass\u00e9 en blanc complet et dans le cas contraire il est pass\u00e9 en noir complet On retourne la Bitmap modifi\u00e9e Un algorythme pas forc\u00e9ment complexe mais qui peut augmenter de mani\u00e8re titanesque les chances de r\u00e9ussir une OCR Resize Cette methode sert \u00e0 augmenter la r\u00e9solution d'une image pour am\u00e9liorer la pr\u00e9cision de l'algorythme de Tesseract. En effet, avec trop peu de pixels, la matrice de convolution n'est pas toujours aussi efficace. Il ne faut pas confondre cette methode d'augmentation de la taille avec une simple interpolation. En effet une augmentation de taille interpol\u00e9e ne vas pas vraiment changer la r\u00e9solution, l'image sera toujours aussi pixelis\u00e9e, seulement, les pixels seront compos\u00e9es de plus de pixels comme dans l'exemple ci dessous : \"Interpolation exemple\" Dans mon projet j'utilise de l'interpolation bicubique qui va cr\u00e9er de l'information pour tenter de combler le vide et produire une image r\u00e9ellement plus grande et avec plus de details mais en ajoutant du flou. \"bicubic exemple\" Le but est d'aller chercher dans les pixels alentours les couleurs qui sont d\u00e9ja pr\u00e9sente et de jouer avec des poids pour tenter de faire une pr\u00e9diction de ce que ce pixel aurait \u00e9t\u00e9 si l'image avait plus de detail. Voici un exemple assez parlant : \"bicubic demonstration\" \"bicubic demonstration\" On pourrait croire que c'est inutile mais dans le contexte de Tesseract ajouter des d\u00e9tails pour tenter de simuler une meilleure r\u00e9solution m\u00eame en cr\u00e9ant du flou est int\u00e9ressant pour mieux remplir la matrice de convolution. Mais il est possible de r\u00e9duire ce flou avec d'autres m\u00e9thodes \u00e9galement. (Dans mon code je n'ai pas utilis\u00e9 du code fait main mais j'utilise une librairie qui me permet de le faire) Il faut simplement faire attention car c'est un proc\u00e9d\u00e9 assez lourd en performances. Dilatation et Erosion Cette methode et la suivante font partie des methodes de transformation morphologiques. Ces methodes sont utilis\u00e9es pour accentuer les formes et les epaissir ou les r\u00e9duire et les affiner. Elles poss\u00e8dent l'aventage \u00e9galement de retirer le flou d'une image ce qui est tr\u00e8s pratique si utilis\u00e9 apr\u00e8s l'utilisation de methodes comme Resize . Je ne vais pas trop rentrer dans les d\u00e9tails de ces methodes car leur fonctionnement est un peu plus lourd en math si on veut faire une v\u00e9ritable explication du pourquoi et du comment ca marche aussi bien. Pour notre projet je dirais que l'important est de savoir que ce sont deux outils tr\u00e8s pratiques pour changer la morphologie des lettres et des chiffres et qu'on peut les utiliser pour corriger du flou et/ou des art\u00e9facts apparus lors de la binarisation de l'image ou de la suppression de fond. Remove Background Cette methode est assez simple et est juste une methode qui va passer en revue tous les pixels de l'image et si la couleur d'un pixel s'apparente \u00e0 celle d'un pixel de fond il est pass\u00e9 en noir total ou en blanc total. Le but est de permettre au reste du programme de fonctionner avec des couleurs moins ambigues. Une variante sp\u00e9cialis\u00e9e pour la reconnaissance des pneus appel\u00e9e affectueusement Remove Useless cherche \u00e0 atteindre le m\u00eame bu mais est bien plus soffistiqu\u00e9e et sp\u00e9cialis\u00e9e pour retirer le background autour d'un cercle de couleur pour ensuite retirer la couleur et qu'il ne reste qu'un chiffre. Pour plus de details voir la detection de pneus. Il y aussi d'autre methodes comme un filtre Gaussien ou Highlight countour que j'ai du d\u00e9velopper mais que je n'ai pas utilis\u00e9 donc je ne vais pas en parler ici.","title":"Filtres et methodes sur les images"},{"location":"index.html#interpretation-des-donnees","text":"","title":"Interpr\u00e9tation des donn\u00e9es"},{"location":"index.html#stockage-des-donnees","text":"","title":"Stockage des donn\u00e9es"},{"location":"index.html#affichage-des-donnees","text":"","title":"Affichage des donn\u00e9es"},{"location":"index.html#predictions","text":"","title":"Pr\u00e9dictions"},{"location":"index.html#tests","text":"[A remplir au fur et \u00e0 mesure de la cr\u00e9ation des tests]","title":"Tests"},{"location":"index.html#resume-des-difficultes-techniques","text":"[A remplir au fur et \u00e0 mesure dans la seconde moiti\u00e9 du travail de dipl\u00f4me]","title":"R\u00e9sum\u00e9 des difficult\u00e9s techniques"},{"location":"index.html#ameliorations-futures","text":"[A remplir dans les derni\u00e8res semaines du travail de dipl\u00f4me]","title":"Am\u00e9liorations futures"},{"location":"index.html#conclusion","text":"[A remplir la derni\u00e8re semaine du travail de dipl\u00f4me]","title":"Conclusion"},{"location":"CahierDesCharges.html","text":"Cahier des charges Cahier des charges \"Track Trends\" Travail de dipl\u00f4me Maxime Rohmer 2023 Contexte Je suis le \"Live Ticker\" charg\u00e9 de la Formule 1 pour le 20 minutes. On peut traduire cela comme commentateur de F1, avec tout de m\u00eame l'importante subtilit\u00e9 que je ne commente pas avec la voix, mais avec le clavier. Mes commentaires sont sous la forme de commentaires \u00e9crits live qui s'ajoutent au fur et \u00e0 mesure de l'\u00e9v\u00e8nement. Par exemple : \"Tour 28/54, Hamilton a fini par s'arr\u00eater et chausser des gommes tendres 13 tours apr\u00e8s Verstappen. L'Anglais va voir plus de 15 secondes \u00e0 rattraper, mais les gommes neuves et plus tendres que son rival devraient lui permettre s'il ne se fait pas trop ralentir par le trafic\". En g\u00e9n\u00e9ral avec un peu plus d'infos quand m\u00eame et cela tous les 3-4 tours Voici quelques exemples de pr\u00e9c\u00e9dents commentaires (Conseil : il y a un bouton pour montrer le feed dans l'ordre chronologique) : \"Commentaire Grand Prix de Belgique 2022\" \"Commentaire du Grand Prix de Singapour 2022\" \"Exemple commentaires\" Pendant un Grand Prix, je dois constamment : \u00c9crire ce qu'il se passe dans le grand prix et expliquer les enjeux Chercher r\u00e9guli\u00e8rement des m\u00e9dias \u00e0 inclure pour diversifier mon live (Tweets, Images etc.) Changer le titre et la description du live en fonction de l'\u00e9volution du Grand prix Et accessoirement regarder le grand prix pour y comprendre quelque chose Avec tout \u00e7a, il est tr\u00e8s difficile de garder un \u0153il sur la page DATA de la F1TV qui fournit pourtant des informations pr\u00e9cieuses. Je me retrouve parfois par exemple \u00e0 ne pas parler de d\u00e9passements dans le peloton, car ils ne sont pas retransmis \u00e0 la t\u00e9l\u00e9 alors que c'est une information importante. Autre exemple, occasionnellement le classement ne refl\u00e8te pas les vraies positions des pilotes. Les arr\u00eats aux stands font que du coup des pilotes qui devraient \u00eatre 15\u00e8mes se retrouvent 8\u1d49 puisqu'ils ne sont pas encore arr\u00eat\u00e9s. Cela peut de temps en temps pr\u00eater \u00e0 confusion. Projet Un outil de style compagnon sous forme d'application C# Windows Form qui r\u00e9cup\u00e8re en temps r\u00e9el les informations de la course et affiche les informations les plus importantes. Le but est non seulement de faciliter mon job, mais aussi faire en sorte d'am\u00e9liorer la plus-value de mon travail en me permettant de fournir des commentaires qui ne sont pas disponibles pour le tout venant qui regarde simplement le flux RTS. Exemples: Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand Maintenant afficher diff\u00e9remment les infos, c'est sympa, mais cela serait encore mieux de traiter ces data et de permettre des petites pr\u00e9dictions. Exemples : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents R\u00e9alisation Malheureusement, la Formula 1 Management ne propose aucune API publique qui puisse nous permettre de faire ce projet \"simplement\". La raison la plus probable \u00e9tant qu'Amazon avec son service AWS propose exactement ce genre de services pour le flux t\u00e9l\u00e9vis\u00e9 et il doit y avoir un contrat d'exclusivit\u00e9. Il existe des API \"Pirates\" faites par la communaut\u00e9, le probl\u00e8me est qu'elles ne sont pas forc\u00e9ment des plus pratiques \u00e0 utiliser. Mais comme je poss\u00e8de un abonnement premium ++ \u00e0 la F1TV, j'ai acc\u00e8s pour chaque grand prix \u00e0 un flux vid\u00e9o nomm\u00e9 : DATA F1 CHANNEL Qui ressemble \u00e0 \u00e7a : \"Data channel exemple\" Donc la seule fa\u00e7on que je vois de r\u00e9cup\u00e9rer ces donn\u00e9es est de les prendre directement sur ce feed. M\u00eame si le but final de l'application est de faire pleins de choses super avec les datas, le gros du projet va surtout \u00eatre la r\u00e9cup\u00e9ration des donn\u00e9es et leur stockage. Les donn\u00e9es viennent du flux vid\u00e9o et ainsi dans un premier temps, il va falloir r\u00e9cup\u00e9rer d'une mani\u00e8re ou d'une autre des images qui viennent d'un grand prix en direct ou en rediffusion. Ensuite, dans un second temps, il faut lire les informations directement sur l'image en utilisant une librairie pr\u00e9vue pour (exemple Tesseract) et v\u00e9rifier l'int\u00e9grit\u00e9 de ces derni\u00e8res pour qu'on puisse ensuite les stocker. Dans un troisi\u00e8me temps, il faut stocker toutes ces donn\u00e9es dans une forme qui permette d'aller facilement faire des requ\u00eates de r\u00e9cup\u00e9ration et d\u00e9j\u00e0 pr\u00e9parer des m\u00e9thodes qui permettent de r\u00e9cup\u00e9rer des infos importantes (ex : la moyenne des cinq derniers tours, le temps moyen d'arr\u00eat etc.) pour faciliter la derni\u00e8re \u00e9tape Quand tout cela est fait, on peut ensuite s'amuser un peu avec les Data. La derni\u00e8re \u00e9tape est donc l'affichage. L'id\u00e9e est de cr\u00e9er une Windows Form qui contienne toutes ces informations dans un format beaucoup plus lisible et avec laquelle on pourrait interagir pour permettre de plus facilement commenter les Grands Prix. (exemple plus bas avec un croquis de ce \u00e0 quoi l'application pourrait ressembler) Voici la liste des donn\u00e9es qui pourraient \u00eatre affich\u00e9es (Non contractuel, simplement des id\u00e9es). Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand La moyenne de temps que les pilotes perdent dans les stands La performance moyenne des 5 types de pneus La moyenne de temps de chaque pilote sur le pneu actuel Le nombre de points que chaque pilote gagnerait selon sa position Le classement de la course Voire m\u00eame si c'est possible : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents Pr\u00e9dire les temps au tour de chaque pilote selon l'usure des pneus Voici un exemple d'interface possible pour une page : \"Proto\" Cas d'utilisation *On va consid\u00e9rer que tous les user ont un abonnement F1 TV PRO Un user veut r\u00e9cup\u00e9rer les data : Il ouvre son navigateur et lance la page DATA de la F1 TV Il calibre la capture des data via le programme (pour la premi\u00e8re utilisation). Il confirme que les donn\u00e9es initiales sont bonnes (pour la premi\u00e8re utilisation). Il regarde tranquille son Grand Prix Le programme r\u00e9cup\u00e8re les data : Il r\u00e9cup\u00e8re des images depuis la F1TV Il utilise Tesseract (ou autre) pour en r\u00e9cup\u00e9rer les infos. Il met ces infos dans un Objet Pilote, dans un Objet course avec un attribut tour pour hi\u00e9rarchiser les data Pour ce qui est de l'affichage, l'id\u00e9e est de faire une application C# comme on l'a appris \u00e0 l'\u00e9cole, mais avec assez de style pour qu'elle puisse \u00eatre agr\u00e9able \u00e0 utiliser. Quand le programme affiche les data : Il prend les donn\u00e9es venant directement de la F1TV. Il affiche diff\u00e9remment les donn\u00e9es pour permettre une meilleure lisibilit\u00e9 Il interpr\u00e8te avec des r\u00e8gles plut\u00f4t simples certaines data pour faire des minipr\u00e9dictions ou aider \u00e0 la lecture Il r\u00e9cup\u00e8re des infos d'autres courses pour les comparer et proposer des pr\u00e9dictions plus int\u00e9ressantes Difficult\u00e9s techniques R\u00e9cup\u00e9rer un flux vid\u00e9o plut\u00f4t propre malgr\u00e9 les contres mesures de la F1 TV pour en emp\u00eacher la lecture par un logiciel Si on doit passer par une capture d'\u00e9cran, trouver un moyen de stocker les donn\u00e9es de mani\u00e8re \u00e0 pr\u00e9voir que parfois un tour pourrait avoir plus de donn\u00e9es qu'un autre, que le user peut mettre pause, ou m\u00eame qu\u2019il revienne en arri\u00e8re. D\u00e9velopper des algorithmes pour r\u00e9cup\u00e9rer les donn\u00e9es comme les diff\u00e9rents pneus utilis\u00e9s ou l'activation du DRS ainsi que d\u00e9velopper des moyens de nettoyer les r\u00e9sultats de l'OCR (Par exemple utiliser diff\u00e9rents champs redondants pour comparer les r\u00e9sultats) Stocker les donn\u00e9es sur une base pour les traiter plus tard tout en pr\u00e9voyant un moyen de voir les stats live D\u00e9velopper des algorithmes de pr\u00e9diction qui prennent en compte d'anciennes courses pour tenter de pr\u00e9dire des choses comme les arr\u00eats aux stands par exemple.","title":"Cahier des charges"},{"location":"CahierDesCharges.html#cahier-des-charges","text":"Cahier des charges \"Track Trends\" Travail de dipl\u00f4me Maxime Rohmer 2023","title":"Cahier des charges"},{"location":"CahierDesCharges.html#contexte","text":"Je suis le \"Live Ticker\" charg\u00e9 de la Formule 1 pour le 20 minutes. On peut traduire cela comme commentateur de F1, avec tout de m\u00eame l'importante subtilit\u00e9 que je ne commente pas avec la voix, mais avec le clavier. Mes commentaires sont sous la forme de commentaires \u00e9crits live qui s'ajoutent au fur et \u00e0 mesure de l'\u00e9v\u00e8nement. Par exemple : \"Tour 28/54, Hamilton a fini par s'arr\u00eater et chausser des gommes tendres 13 tours apr\u00e8s Verstappen. L'Anglais va voir plus de 15 secondes \u00e0 rattraper, mais les gommes neuves et plus tendres que son rival devraient lui permettre s'il ne se fait pas trop ralentir par le trafic\". En g\u00e9n\u00e9ral avec un peu plus d'infos quand m\u00eame et cela tous les 3-4 tours Voici quelques exemples de pr\u00e9c\u00e9dents commentaires (Conseil : il y a un bouton pour montrer le feed dans l'ordre chronologique) : \"Commentaire Grand Prix de Belgique 2022\" \"Commentaire du Grand Prix de Singapour 2022\" \"Exemple commentaires\" Pendant un Grand Prix, je dois constamment : \u00c9crire ce qu'il se passe dans le grand prix et expliquer les enjeux Chercher r\u00e9guli\u00e8rement des m\u00e9dias \u00e0 inclure pour diversifier mon live (Tweets, Images etc.) Changer le titre et la description du live en fonction de l'\u00e9volution du Grand prix Et accessoirement regarder le grand prix pour y comprendre quelque chose Avec tout \u00e7a, il est tr\u00e8s difficile de garder un \u0153il sur la page DATA de la F1TV qui fournit pourtant des informations pr\u00e9cieuses. Je me retrouve parfois par exemple \u00e0 ne pas parler de d\u00e9passements dans le peloton, car ils ne sont pas retransmis \u00e0 la t\u00e9l\u00e9 alors que c'est une information importante. Autre exemple, occasionnellement le classement ne refl\u00e8te pas les vraies positions des pilotes. Les arr\u00eats aux stands font que du coup des pilotes qui devraient \u00eatre 15\u00e8mes se retrouvent 8\u1d49 puisqu'ils ne sont pas encore arr\u00eat\u00e9s. Cela peut de temps en temps pr\u00eater \u00e0 confusion.","title":"Contexte"},{"location":"CahierDesCharges.html#projet","text":"Un outil de style compagnon sous forme d'application C# Windows Form qui r\u00e9cup\u00e8re en temps r\u00e9el les informations de la course et affiche les informations les plus importantes. Le but est non seulement de faciliter mon job, mais aussi faire en sorte d'am\u00e9liorer la plus-value de mon travail en me permettant de fournir des commentaires qui ne sont pas disponibles pour le tout venant qui regarde simplement le flux RTS. Exemples: Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand Maintenant afficher diff\u00e9remment les infos, c'est sympa, mais cela serait encore mieux de traiter ces data et de permettre des petites pr\u00e9dictions. Exemples : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents","title":"Projet"},{"location":"CahierDesCharges.html#realisation","text":"Malheureusement, la Formula 1 Management ne propose aucune API publique qui puisse nous permettre de faire ce projet \"simplement\". La raison la plus probable \u00e9tant qu'Amazon avec son service AWS propose exactement ce genre de services pour le flux t\u00e9l\u00e9vis\u00e9 et il doit y avoir un contrat d'exclusivit\u00e9. Il existe des API \"Pirates\" faites par la communaut\u00e9, le probl\u00e8me est qu'elles ne sont pas forc\u00e9ment des plus pratiques \u00e0 utiliser. Mais comme je poss\u00e8de un abonnement premium ++ \u00e0 la F1TV, j'ai acc\u00e8s pour chaque grand prix \u00e0 un flux vid\u00e9o nomm\u00e9 : DATA F1 CHANNEL Qui ressemble \u00e0 \u00e7a : \"Data channel exemple\" Donc la seule fa\u00e7on que je vois de r\u00e9cup\u00e9rer ces donn\u00e9es est de les prendre directement sur ce feed. M\u00eame si le but final de l'application est de faire pleins de choses super avec les datas, le gros du projet va surtout \u00eatre la r\u00e9cup\u00e9ration des donn\u00e9es et leur stockage. Les donn\u00e9es viennent du flux vid\u00e9o et ainsi dans un premier temps, il va falloir r\u00e9cup\u00e9rer d'une mani\u00e8re ou d'une autre des images qui viennent d'un grand prix en direct ou en rediffusion. Ensuite, dans un second temps, il faut lire les informations directement sur l'image en utilisant une librairie pr\u00e9vue pour (exemple Tesseract) et v\u00e9rifier l'int\u00e9grit\u00e9 de ces derni\u00e8res pour qu'on puisse ensuite les stocker. Dans un troisi\u00e8me temps, il faut stocker toutes ces donn\u00e9es dans une forme qui permette d'aller facilement faire des requ\u00eates de r\u00e9cup\u00e9ration et d\u00e9j\u00e0 pr\u00e9parer des m\u00e9thodes qui permettent de r\u00e9cup\u00e9rer des infos importantes (ex : la moyenne des cinq derniers tours, le temps moyen d'arr\u00eat etc.) pour faciliter la derni\u00e8re \u00e9tape Quand tout cela est fait, on peut ensuite s'amuser un peu avec les Data. La derni\u00e8re \u00e9tape est donc l'affichage. L'id\u00e9e est de cr\u00e9er une Windows Form qui contienne toutes ces informations dans un format beaucoup plus lisible et avec laquelle on pourrait interagir pour permettre de plus facilement commenter les Grands Prix. (exemple plus bas avec un croquis de ce \u00e0 quoi l'application pourrait ressembler) Voici la liste des donn\u00e9es qui pourraient \u00eatre affich\u00e9es (Non contractuel, simplement des id\u00e9es). Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand La moyenne de temps que les pilotes perdent dans les stands La performance moyenne des 5 types de pneus La moyenne de temps de chaque pilote sur le pneu actuel Le nombre de points que chaque pilote gagnerait selon sa position Le classement de la course Voire m\u00eame si c'est possible : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents Pr\u00e9dire les temps au tour de chaque pilote selon l'usure des pneus Voici un exemple d'interface possible pour une page : \"Proto\"","title":"R\u00e9alisation"},{"location":"CahierDesCharges.html#cas-dutilisation","text":"*On va consid\u00e9rer que tous les user ont un abonnement F1 TV PRO Un user veut r\u00e9cup\u00e9rer les data : Il ouvre son navigateur et lance la page DATA de la F1 TV Il calibre la capture des data via le programme (pour la premi\u00e8re utilisation). Il confirme que les donn\u00e9es initiales sont bonnes (pour la premi\u00e8re utilisation). Il regarde tranquille son Grand Prix Le programme r\u00e9cup\u00e8re les data : Il r\u00e9cup\u00e8re des images depuis la F1TV Il utilise Tesseract (ou autre) pour en r\u00e9cup\u00e9rer les infos. Il met ces infos dans un Objet Pilote, dans un Objet course avec un attribut tour pour hi\u00e9rarchiser les data Pour ce qui est de l'affichage, l'id\u00e9e est de faire une application C# comme on l'a appris \u00e0 l'\u00e9cole, mais avec assez de style pour qu'elle puisse \u00eatre agr\u00e9able \u00e0 utiliser. Quand le programme affiche les data : Il prend les donn\u00e9es venant directement de la F1TV. Il affiche diff\u00e9remment les donn\u00e9es pour permettre une meilleure lisibilit\u00e9 Il interpr\u00e8te avec des r\u00e8gles plut\u00f4t simples certaines data pour faire des minipr\u00e9dictions ou aider \u00e0 la lecture Il r\u00e9cup\u00e8re des infos d'autres courses pour les comparer et proposer des pr\u00e9dictions plus int\u00e9ressantes","title":"Cas d'utilisation"},{"location":"CahierDesCharges.html#difficultes-techniques","text":"R\u00e9cup\u00e9rer un flux vid\u00e9o plut\u00f4t propre malgr\u00e9 les contres mesures de la F1 TV pour en emp\u00eacher la lecture par un logiciel Si on doit passer par une capture d'\u00e9cran, trouver un moyen de stocker les donn\u00e9es de mani\u00e8re \u00e0 pr\u00e9voir que parfois un tour pourrait avoir plus de donn\u00e9es qu'un autre, que le user peut mettre pause, ou m\u00eame qu\u2019il revienne en arri\u00e8re. D\u00e9velopper des algorithmes pour r\u00e9cup\u00e9rer les donn\u00e9es comme les diff\u00e9rents pneus utilis\u00e9s ou l'activation du DRS ainsi que d\u00e9velopper des moyens de nettoyer les r\u00e9sultats de l'OCR (Par exemple utiliser diff\u00e9rents champs redondants pour comparer les r\u00e9sultats) Stocker les donn\u00e9es sur une base pour les traiter plus tard tout en pr\u00e9voyant un moyen de voir les stats live D\u00e9velopper des algorithmes de pr\u00e9diction qui prennent en compte d'anciennes courses pour tenter de pr\u00e9dire des choses comme les arr\u00eats aux stands par exemple.","title":"Difficult\u00e9s techniques"},{"location":"jdb.html","text":"Journal de bord Mercredi 29 Mars 2023 Premier jour du travail de dipl\u00f4me. Nous avons eu un briefing de mr Garcia et nous avons pu commencer \u00e0 pr\u00e9parer le travail. Nous avons eu les diff\u00e9rents fichiers nescessaires \u00e0 la bonne r\u00e9alisation du projet et je me suis mis \u00e0 faire les fichiers nescessaires La premi\u00e8re chose a \u00e9t\u00e9 de faire ce mkdocs dans lequel j'ai mis un fichier yml plut\u00f4t standart qui risque de changer au fur et \u00e0 mesure. Voici le premier yml : site_name: Documentation Diplome theme: name: material palette: # Palette toggle for light mode - media: \"(prefers-color-scheme: light)\" scheme: default toggle: icon: material/brightness-7 name: Switch to dark mode # Palette toggle for dark mode - media: \"(prefers-color-scheme: dark)\" scheme: slate toggle: icon: material/brightness-4 name: Switch to light mode markdown_extensions: - attr_list - md_in_html plugins: - glightbox - with-pdf Voici un example de \u00e0 quoi ca ressemble en forme de site \"Exemple mkdocs\" Ensuite il m'a fallu faire une version plus \u00e0 jour de mon cahier des charges car je n'y avait pas touch\u00e9 depuis novembre. J'ai envoy\u00e9 un mail \u00e0 mes enseignants pour qu'ils puissent y jeter un oeuil pour \u00eatre s\u00fbr que je n'ai rien chang\u00e9 qui les d\u00e9rangent. Monsieur Jayr m'a demad\u00e9 \u00e0 l'occasion de lui faire un planning type Gantt alors je me suis mis \u00e0 la t\u00e2che. J'ai fait un planning pr\u00e9visionnel et une l\u00e9gende les deux sont dispo dans le dossier planning de ce repertoire. Ensuite je me suis mis \u00e0 tout mettre sur git. A commencer par ce repertoire Et c'est deja la fin de la journ\u00e9e ! Demain j'avance un peu sur la doc avec ce que je peux d\u00e9ja remplir et je finis de pr\u00e9parer ce dont j'ai besoin pour commencer \u00e0 coder. Jeudi 30 Mars 2023 Aujourd'hui selon le planning je dois me charger des dernirers pr\u00e9paratifs pour commencer correctement. J'ai fait expr\u00e8s de prenre du temps pour ca au d\u00e9but pour ne pas me cr\u00e9er de soucis plus loin pendant le travail. Je vais envoyer par mail le planning que j'ai fait \u00e0 mes suiveurs. Ensuite je vais m'attaquer au squelette de la docmentation. Je vais essayer de remplir tout ce que je peux remplir dans un premier temps car cela tout ca de fait pour plus tard quitte \u00e0 modifier quelques aspects au fur et \u00e0 mesure. J'ai aussi d\u00e9sactiv\u00e9 mkdocs with pdf par ce que les r\u00e9sultats ne sont vraiment pas ceux que j'attends et cela ralentis \u00e9norm\u00e9ment le d\u00e9ploiment. J'ai aussi rassembl\u00e9 mes croquis pour le poster : \"Croquis Poster 1\" \"Croquis Poster 2\" On peut voir que dans un premier temps j'ai tent\u00e9 de faire un poster un peu plus stylis\u00e9 et marketing. Cependant apr\u00e8s avoir discut\u00e9 avec Mr Garcia et diff\u00e9rents profs dont un de l'HEPIA et ils m'ont indiqu\u00e9 que ce qui \u00e9tait attendu \u00e9tait moins du marketing qu'un diagramme de fonctionnement. On peut voir sur les derniers posters que le cot\u00e9 technique ressort de plus en plus. Le but sera de faire une version encore plus technique ou on peut voir les diff\u00e9rents fonctionnements de l'application avec les technologies utilis\u00e9es. Le d\u00e9fi cela va \u00eatre de faire un joli poster qui soit en m\u00eame temps vendeur et en m\u00eame temps rempli techniquement. Oh et j'ai eu un probl\u00e8me ou mon calvier et ma souris ne voulaient d'un coup plus fonctionner. Dans mon cas c'\u00e9tait un probl\u00e8me de power management des ports. J'ai eu le soucis sur mon pc fixe \u00e0 la maison et sur mon pc portable \u00e9galement. En gros de ce que j'ai compris le soucis c'est que le pc croit que un port est trop solicit\u00e9 niveau puissance et du coup d\u00e9cide de couper l'alimentation du port USB. J'ai pu r\u00e8gler le soucis en allant dans le device manager sous universal bus controller sous power management et en d\u00e9cochant la case qui indique que windows peut d\u00e9sactiver ce port. Je ne conseille pas ce fix si vous avez des composants de mauvaise qualit\u00e9 car cela pourrait \u00eatre une vraie alerte cependant le fait que mes composants sont plut\u00f4t haut de gamme et le fait que mon clavier et ma souris le fassent en m\u00eame temps et que ils fonctionnaient tr\u00e8s bien depuis plus de 4 ans me font penser que c'est juste une nouvelle mise a jour de windows qui est p\u00e9nible. Demain je vais pouvoir commencer \u00e0 coder pour de bon. Vendredi 31/03/2023 Aujourd'hui on s'occupe de la PT2 qui est la programmation de la r\u00e9cup\u00e8ration des informations des images. Je vais tester IronOcr Source : https://www.c-sharpcorner.com/article/ocr-using-tesseract-in-C-Sharp/ Doc : https://ironsoftware.com/csharp/ocr/docs/ Examples : https://ironsoftware.com/csharp/ocr/examples/simple-csharp-ocr-tesseract/ Avant d'utiliser la librairie je me demande si je dois utiliser un peu de post processing pour aider \u00e0 la reconnaissance. Je peux soit utiliser l'image crop\u00e9e directement : \"Image non trait\u00e9e\" Soit avec un filtre pour passer en noir et blanc laxiste \"Image trait\u00e9e laxiste\" Soit avec un filtre pour passer en noir et blanc stricte \"Image trait\u00e9e stricte\" Il va falloir faire des tests avec tous les noms et les chiffres pour trouver le plus efficace. Bon malheureusment Iron OCR semblait \u00eatre une bonne alternative mais c'est une librairie priv\u00e9e qui demande une license pour \u00eatre utilis\u00e9e. Il va falloir trouver autre chose. En utilisant la librairie \"Tesseract\" qui existe on peut faire de la reconnaissance de texte avec un code plut\u00f4t simple : TesseractEngine engine = new TesseractEngine(tessDataFolder.FullName,\"eng\", EngineMode.Default); var tessImage = Pix.LoadFromMemory(ImageToByte(sample)); Page page = engine.Process(tessImage); string text = page.GetText(); Voici la methode ImageToByte : https://stackoverflow.com/questions/7350679/convert-a-bitmap-into-a-byte-array public static byte[] ImageToByte(Image img) { using (var stream = new MemoryStream()) { img.Save(stream, System.Drawing.Imaging.ImageFormat.Png); return stream.ToArray(); } } Voici le code pour traiter plusieurs textes sur une seule image : Page page = engine.Process(tessImage); // Get the iterator for the page layout using (var iter = page.GetIterator()) { // Loop over the elements of the page layout iter.Begin(); do { // Declare a Rect variable to hold the bounding box Rect boundingBox; // Get the bounding box for the current element if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox)) { g.DrawRectangle(Pens.Red,new Rectangle(boundingBox.X1,boundingBox.Y1,boundingBox.Width,boundingBox.Height)); } // Get the text for the current element var text = iter.GetText(PageIteratorLevel.Word); tbxResult.Text += text.ToUpper() + Environment.NewLine; } while (iter.Next(PageIteratorLevel.Word)); } Etonnament, avec plus de texte, des noms qui \u00e9taient autrefois mal reconnus sont parfaitement interpr\u00eat\u00e9s. Par exemple voici un exemple de reconnaisance de texte sur tous les pilotes : \"Screenshot de reconnaisance d'image complete\" On voit que le nom Leclerc est mal reconnu. Mais voici ce que cela donne quand on prend une image qui ne contient que le nom Leclerc : \"Screenshot de reconnaissance d'image crop\u00e9e\" On voit ici que le nom Leclerc est tr\u00e8s bien reconnu. Dans le premier exemple on peut voir que Tsunoda est reconnu comme \"Reticin\" ce qui n'est pas exactement pareil (lol) Et quand on isole le nom Tsunoda dans une image seule : \"Screenshot de reconnaissance de Tsunoda\" Il le lit \"RETLELYY\" ce qui n'est toujours pas exactement ca... Une meilleure r\u00e9solution pourrait peut-\u00eatre r\u00e9soudre le probl\u00e8me en partie. Jusqu'ici les images \u00e9taient en presque 720P ce qui donne ceci : \"Tsunoda en 720P\" Et j'ai lanc\u00e9 une r\u00e9cup\u00e8ration d'images en 1080p pour r\u00e9cup\u00e8rer ceci : \"Tsunoda en 1080P\" On peut voir une certaine diff\u00e9rence tout de m\u00eame. Et quand on lance la reconnaissance : \"Reconnaissance Tsunoda en 1080P\" \"Tsunoda n'est plus \u00e9crit \"RETLELYY\" mais \"TSUNDDA\" ce qui n'est pas parfait mais qui est d\u00e9ja beaucoup mieux. J'ai essay\u00e9 de mettre l'engine de Tesseract en mode \"JPN\" comme Tsunoda est un nom japonais mais sans succ\u00e8s j'ai le m\u00eame r\u00e9sultat. Comme la r\u00e9solution est meilleure je me suis dit que peut \u00eatre le filtre de passage en noir et blanc pourrait aider. J'ai \u00e9crit cette petite methode pour convertir l'image en noir et blanc : private static Bitmap ConvertToBlackAndWhite(Bitmap inputBmp) { const int BLACK_TO_WHITE_TRESHOLD = 200; Bitmap result = new Bitmap(inputBmp.Width, inputBmp.Height); for (int y = 0; y < inputBmp.Height; y++) { for (int x = 0; x < inputBmp.Width; x++) { Color pixelColor = inputBmp.GetPixel(x,y); if (pixelColor.R <= BLACK_TO_WHITE_TRESHOLD && pixelColor.G <= BLACK_TO_WHITE_TRESHOLD && pixelColor.B <= BLACK_TO_WHITE_TRESHOLD) { pixelColor = Color.FromArgb(0,0,0); } else { pixelColor = Color.FromArgb(255,255,255); } result.SetPixel(x,y,pixelColor); } } return result; } Rien de bien dingue mais cela fonctionne et je peux jouer avec le BLACK_AND_WHITE_TRESHOLD pour changer son comportement. J'ai dabord test\u00e9 avec un treshold de 100 et le programme a r\u00e9ussi \u00e0 me sortir Tsunoda en deux mots ce qui \u00e9tait d\u00e9ja tr\u00e8s encourageant. Et apr\u00e8s avoir augment\u00e9 le Treshold... Tada : \"Tsunoda 1080P avec filtre\" Le programme arrive bien \u00e0 reconnaitre TSUNODA. Je pense que cette tactique ne fonctionnait pas avant car la resolution \u00e9tait trop faible et l'aliasing se m\u00ealait trop avec le texte pour \u00eatre utilisable. Cependant cette technique ne fonctionne pas sur tous les noms. Par example avec Leclerc : \"Leclerc 1080P avec filtre\" On r\u00e9cup\u00e8re \"Leeler'c\" ce qui n'est pas bon du tout. Mais en modulant le Treshold (ici \u00e0 150) On peut de nouveau voir Leclerc \u00eatre reconnu correctement \"Leclerc 1080P avec filtre 2\" Je pense que pour avoir de bons r\u00e9sultats il va falloir faire un algo qui : D\u00e9coupe l'image en autant de plus petites images pour avoir un mot par image. Teste voir si avec l'image originale un nom correspond \u00e0 la liste de pilotes existant. Si cela ne marche pas, on applique le filtre en modulant le Treshold. Dans le cas ou on aurait pas un match parfait on fait un algo qui cherche le nom le plus proche qui existe dans la liste de noms donn\u00e9s. Seulement voila, il n'y a pas que des lettres que l'on veut r\u00e9cup\u00e8rer. On veut surtout pouvoir r\u00e9cup\u00e8rer les chiffres. Pour les chiffres on va avoir des soucis \u00e9galement... Si on essaie directement la m\u00eame technique sans filtre on a des r\u00e9sultats comme celui ci : \"Tentative de reconnaisance du timing\" La virgule a tendeance \u00e0 se barrer ce qui est particuli\u00e8rement probl\u00e9matique. Cependant comme les chiffres ont beaucoup moins de possibilit\u00e9es que les lettres et qu'il n'y a pas de probl\u00e8me de langue on devrait pouvoir travailler \u00e0 faire des r\u00e8glage que l'on pourra ensuite utiliser. Avec un Treshold de 165 on arrive presque \u00e0 quelque chose d'int\u00e9ressant : \"Tentative 2 de reconnaissance du timing\" Le + n'est clairement pas compris mais ca n'est pas emb\u00eatant car c'est souvent redondant. On arrive cependant \u00e0 isoler 3 et 259. M\u00eame si la virgule n'est pas comprise cela veut dire qu'il est tout de m\u00eame possible de discriminer les secondes des milisecondes. Maintenant avec un temps au tour : \"Reconnaissance du timing au tour\" On arrive sans rien changer aux param\u00eatres \u00e0 isoler minutes secondes et milisecondes. Il semble que la reconnaissance de chiffre soit bien plus efficace que la reconnaissance de lettres. Il va falloir faire un test \u00e0 plus grande \u00e9chelle avec plus d'image pour se rendre compte de la precision. Demain ce qui serait bien cela serait que je fasse un jeu d'images avec des valeurs connues et que je fasse une batterie de tests pour voir \u00e0 quel point je peux faire confiance \u00e0 la reconnaissance des chiffres. Automatiser un syst\u00e8me de test de la sorte me sera tr\u00e8s utile dans le futur pour v\u00e9rifier la non regression de ma reconnaissance de texte quand je tenterai d'y faire des changements. Je suis toujours curieux cependant de voir comment le programme se d\u00e9brouille avec les nombres de tours qui se trouvent dans les icones de pneus. Lundi 3 Avril Aujourd'hui on va faire un programme qui permet de cr\u00e9er un dataset qui permette de tester le programme de reconnaissance. Je pense que le meilleur moyen de faire serait un programme qui cr\u00e9e le dataset et qui ensuite peut tester diff\u00e9rentes methodes de reconnaissance. Par la m\u00eame occasion je peux d\u00e9velopper la technologie qui va permettre de d\u00e9couper une image en 20 lignes ce qui me servira ensuite pour la reconnaissance. Je me rend compte que pour faire un programme de tests je dois d\u00e9ja avoir une id\u00e9e de la structure de mon programme. Pour le moment je r\u00e9flechis \u00e0 un syst\u00e8me de \"Zones\" et de \"Windows\". L'id\u00e9e serait que une Zone est juste une sous partie d'image qui peut encore \u00eatre d\u00e9compos\u00e9 tandis que chaque Window contient une ou plusieurs informations \u00e0 r\u00e9cup\u00e8rer. J'ai essay\u00e9 de d\u00e9couper l'image pour que cela soit plus clair : \"Main zone\" Ici on peut voir que l'image est d\u00e9coup\u00e9e en plusieurs grandes zones. Dans un premier temps on ne s'occupe que de la premi\u00e8re. Ensuite : \"Driver zone #1\" On peut voir la que cette Main zone serait elle m\u00eame d\u00e9compos\u00e9e en plusieurs plus petites zones. Et ensuite chacunes de ces petites zones : \"Driver windows\" Sera d\u00e9compos\u00e9e en plusieurs windows qui elles sont des zones qui contiennent de l'information. En gros on aurait trois types de zone : Les zones qui contiennent d'autres zones Les zones qui contiennent des Windows Les Windows Cependant en y r\u00e9flechissant on pourrait tout \u00e0 fait avoir seulement des zones et des windows en faisant en sorte que les windows peuvent avoir une liste de windows et une liste de zones. Une zone serait compos\u00e9e de : Une image de d\u00e9part Un rectangle qui la positionne sur cette derni\u00e8re Une liste de zones (potentiellement vide) Une liste de windows (potentiellement vide) Une methode qui permet de r\u00e9cup\u00e8rer une image de la zone Une methode qui permet de lancer la reconnaissance sur chaque window Une window serait compos\u00e9e de : Une image de d\u00e9part (cela peut \u00eatre l'image crop\u00e9e de la zone parente peu importe) Un rectangle qui la positionne sur cette derni\u00e8re Une methode qui permet de r\u00e9cup\u00e9rer un image de la window Une methode qui permet de lancer la reconnaisance sur l'image (Chaque type de zone doit l'impl\u00e9menter) Dans chaque window on peut imaginer que la methode qui fait la reconnaissance au lieu de retourner un objet qui peut contenir nimporte quel type d'information peut envoyer ce qu'elle vient de r\u00e9cup\u00e8rer dans une base de donn\u00e9e ou un objet. Par exemple une Zone de pilote pourrait tr\u00e8s bien contenir un objet pilote et le donner \u00e0 ses windows qui rempliraient ce m\u00eame objet. C'est une reflexion plus stockage que OCR mais c'est int\u00e9ressant pour savoir ce que fait une window des donn\u00e9es qu'elle r\u00e9cup\u00e8re. Dans un premier temps je pense que les windows vont simplement \u00e9crire dans un fichier ce qu'elles trouvent chacunes dans le format qu'elles veulent. Pour comprendre pourquoi je me prend la t\u00eate il faut savoir que chaque window peut avoir acc\u00e8s \u00e0 pleins d'informations diff\u00e9rentes. On pourrait dire qu'elles retournent toutes une string sauf que si ca marche pour un temps au tour ou pour un nom de pilote, cela ne marche pas forc\u00e9ment pour un type de pneu ou un DRS ouver. Comme chaque window a plusieurs types de data elle devra elle m\u00eame se charger de comment la traiter ET de la stocker. Voila un diagramme qui r\u00e9sume comment je vois l'impl\u00e9mentation dans un premier temps : \"Diagramme d'explications\" Voici comment se pr\u00e9sente le squellette d'une Zone : public class Zone { private Bitmap FullImage; private List Zones; private List Windows; private Rectangle _bounds; public Rectangle Bounds { get => _bounds; private set => _bounds = value; } public Bitmap ZoneImage { get { Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(FullImage, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); return sample; } } public Zone(Image fullImage, Rectangle bounds) { FullImage = (Bitmap)fullImage; Init(bounds); } public Zone(Bitmap fullImage, Rectangle bounds) { FullImage = fullImage; Init(bounds); } private void Init(Rectangle bounds) { Bounds = bounds; Zones = new List(); Windows = new List(); } public void AddZone(Rectangle bounds) { if(Fits(bounds)) Zones.Add(new Zone(ZoneImage,bounds)); } public void AddWindow(Rectangle bounds) { if (Fits(bounds)) Windows.Add(new Window(ZoneImage,bounds)); } private bool Fits(Rectangle inputRectangle) { if (inputRectangle.X + inputRectangle.Width > Bounds.Width || inputRectangle.Y + inputRectangle.Height > Bounds.Height || inputRectangle.X < 0 || inputRectangle.Y < 0) { return false; } else { return true; } } } Le but est ensuite de cr\u00e9er diff\u00e9rent types de Zones. Par exemple la MainZone devra d\u00e9couper son contenu en 20 parties \u00e9gales pour tenter de chopper les 20 pilotes. Il serait cool de trouver un moyen de calibrer automatiquement. C'est peut-\u00eatre possible de calibrer avec de la reconnaissance de texte, on peut essayer de lancer la reconnaissance et voir ou on trouve du texte avec un peu de chance cela pourrait donner les positions et avec ca on peut peut-\u00eatre determiner des lignes. Et voici le squelette d'une window g\u00e9n\u00e9rique using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace OCR_tester { public class Window { private Bitmap FullImage; private Rectangle _bounds; public Rectangle Bounds { get => _bounds; private set => _bounds = value; } public Bitmap WindowImage { get { Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(FullImage, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); return sample; } } public Window(Bitmap fullImage, Rectangle bounds) { FullImage = fullImage; Bounds = bounds; } public virtual void RecoverInformations() { //Each Window type will have to implement its own way to recover the informations stored in the Window Image } } } Chaque Window pourra ainsi elle m\u00eame impl\u00e9menter la r\u00e9cup\u00e8ration d'informations. La facon de les retourner/stocker est encore un peu floue. Par exemple pour un temps au tour on peut imaginer que il fait une petite v\u00e9rification dans l'objet pilote et dans le tableau des tours si il n'y a pas deja une valeur et si il n'y en a pas une alors il peut l'ajouter. Maintenant je vais essayer de cr\u00e9er une Main window qui se calibre toute seule. Alors apr\u00e8s avoir bien gal\u00e8r\u00e9 avec l'interface pour permettre au user de cliquer sur la form pour voir les zones qu'il cr\u00e9e, j'ai pu cr\u00e9er un zone qui fait les dimensions de MainZone et j'ai pu lancer la reconnaissance sur l'image et voir ou il trouve du texte : \"MainZone avec carr\u00e9s de texte\" Maintenant il faut que je nettoie la liste de rectangle pour exclure ceux qui sont trop grands pour \u00eatre sur une seule ligne, ceux qui indiquent le nombre de tour en haut et ceux qui n'ont pas d'int\u00e9r\u00eats. On pourra ensuite isoler les lignes et cr\u00e9er une liste d'images. Pour ce qui est de la ligne qui contient les \"Gap interval last lap\" et des chiffres sur les tours pour les pneus etc je vais juste demander \u00e0 l'utilisateur de ne pas les prendre dans la screenshot. Comme ils contiennent des mots qui peuvent \u00eatre utilis\u00e9s plus loin dans les data je ne peux pas les blacklister et faire un syst\u00e8me qui s'occupe de les enlever si ils existent selon le position y me prendrait trop de temps pour rien. Apr\u00e8s avoir filtr\u00e9 un peu les resultats et enlev\u00e9 les zones beaucoup trop grandes, on se retrouve d\u00e9ja plus qu'avec ca : \"MainZone avec de meilleurs carr\u00e9s\" Comme on peut le voir, du c\u00f4t\u00e9 gauche de l'image on a beaucoup de choses reconnues mais avec beaucoup de tailles diff\u00e9rentes ce qui n'est pas id\u00e9al. Alors j'ajoute un filtre qui permet de ne selectionner que les data sur la droite. \"MainZone avec de meilleurs carr\u00e9s\" Maintenant il devrait \u00eatre possible de faire un algorythme qui ne prend que un seul carr\u00e9 par ligne. \"MainZone avec un seul carr\u00e9 par ligne\" Maintenant que on sait ou se trouve chaque ligne on peut faire un petit traitement et d\u00e9couper l'image en plusieurs windows. Et voila : \"Mainzone auto calibr\u00e9e\" Maintenant le programme peut cr\u00e9er des zones pour chaque pilote \"Images pilotes\" \"Zone d'un pilote\" Maintenant il faut que j'impl\u00e9mente un syst\u00e8me un peu similaire pour cr\u00e9er des windows. Voici la methode que j'ai cr\u00e9\u00e9 pour l'autocalibration : public void AutoCalibrate() { List detectedText = new List(); Zones = new List(); TesseractEngine engine = new TesseractEngine(Window.tessDataFolder.FullName, \"eng\", EngineMode.Default); Image image = ZoneImage; var tessImage = Pix.LoadFromMemory(Window.ImageToByte(image)); Page page = engine.Process(tessImage); using (var iter = page.GetIterator()) { iter.Begin(); do { Rect boundingBox; if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox)) { //var text = iter.GetText(PageIteratorLevel.Word).ToUpper(); //We remove all the rectangles that are definitely too big if (boundingBox.Height < image.Height / NUMBER_OF_DRIVERS) { //Now we add a filter to only get the boxes in the right because they are much more reliable in size if (boundingBox.X1 > image.Width / 2) { //Now we check if an other square box has been found roughly in the same y axis bool match = false; //The tolerance is roughly half the size that a window will be int tolerance = (image.Height / NUMBER_OF_DRIVERS) / 2; foreach (Rectangle rect in detectedText) { if (rect.Y > boundingBox.Y1 - tolerance && rect.Y < boundingBox.Y1 + tolerance) { //There already is a rectangle in this line match = true; } } //if nothing matched we can add it if(!match) detectedText.Add(new Rectangle(boundingBox.X1, boundingBox.Y1, boundingBox.Width, boundingBox.Height)); } } } } while (iter.Next(PageIteratorLevel.Word)); } foreach (Rectangle Rectangle in detectedText) { Rectangle windowRectangle; Size windowSize = new Size(image.Width, image.Height / NUMBER_OF_DRIVERS); Point windowLocation = new Point(0, (Rectangle.Y + Rectangle.Height / 2) - windowSize.Height / 2); windowRectangle = new Rectangle(windowLocation, windowSize); Zones.Add(new Zone(ZoneImage, windowRectangle)); } } Ca peut paraitre pas \u00e9norme comme code mais pour tout mettre en place ca demande quand m\u00eame pas mal de reflexion. J'ai du clean un peu le code que j'avais fait pour permettre la selection de zones et ajouter la possibilit\u00e9 d'ajouter des windows sur une zone. J'ai juste quelques difficult\u00e9es \u00e0 les ajouter correctement, j'ai un offset tout pourri qui se met tout le temps \"Sainz coup\u00e9\" \"Perez coup\u00e9\" Cela doit \u00eatre un soucis lors de la detection de clic qui met un offset en trop. C'est vraiment p\u00e9nible en tout cas. Certes c'est moins fun de devoir manuellement indiquer ou sont les windows sur une ligne de pilote, mais je ne vois vraiment pas comment faire cela automatiquement. Le but c'est de faire une configuration qui puisse \u00eatre sauvegard\u00e9e comme ca pas besoin d'\u00e0 chaques fois le refaire. C'est bon ! J'avais juste oubli\u00e9 de changer le calcul d'offset entre le code de la zone et de la window. Note pour plus tard, il serait peut-\u00eatre judicieux de faire quelque chose pour la vue, les windows et les Zones ont le m\u00eame exact comportement pour la vue ce qui fait dupliquer du code. Mais au moins maintenant ca fonctionne : \"Ocr tester screenshot\" Et le programme va directement cr\u00e9er un dossier par pilote avec toutes les images de chaque Data le concernant : \"Dossier Perez\" ; Et c'est tout pour aujourd'hui je pense. Ce qui serait cool demain c'est que je puisse stocker d'une mani\u00e8re ou d'une autre ces fichiers de calibration et que je puisse les transf\u00e8rer vers le programme qui va s'occuper de d\u00e9coder et commencer gentillement \u00e0 d\u00e9coder les diff\u00e9rents types de data. Note pour quand je ferai les tests. Je pense que la meilleure id\u00e9e serait que je prenne pleins de photos du style et que je les mette dans un fichier CSV ou JSON avec leur contenu. Et ensuite je le fais passer en tests pour calculer la prescision de mon algo de d\u00e9codage. Pour le moment on est plut\u00f4t dans les clouts niveau planning. Mardi 4 Avril Aujourd'hui je suis scens\u00e9 plut\u00f4t bosser sur l'interpretation des donn\u00e9es, mais une id\u00e9e m'a taraud\u00e9 l'esprit toute la nuit. Est-ce que je ne pourrais pas quand m\u00eame essayer de d\u00e9composer la zone de pilote directment comme pour la Main zone. Pour ce faire j'ai tent\u00e9 de faire comme pour la main zone c'est \u00e0 dire lancer la reconnaissance pour savoir ou \u00e9taient tous les champs de donn\u00e9es mais malheureusement je ne pense pas que cela va \u00eatre possible. En effet non seulement ici les champs sont de tailles tr\u00e8s vari\u00e9es, mais en plus la reconnaissance n'arrive pas \u00e0 en r\u00e9cup\u00e8rer le m\u00eame nombre sur chaque ligne ce qui risque d'\u00eatre complexe \u00e0 utiliser ensuite. La preuve : \"Tentative d'auto calibration\" ; Cependant tout n'est pas perdu ! Il y a peut-\u00eatre un moyen qui serait mieux en tous points. Le soucis avec ce type de reconnaissance c'est qu'on utilise beaucoup de ressources inutiles. On peut peut-\u00eatre hard coder la valeur des diviseurs et les utiliser pour cr\u00e9er des zones. Ok alors visiblement c'est un probl\u00e8me car il semble y avoir d'autres pixels de cette couleur dans l'image (Qui l'aurait cru lol) \"Tentative 2\" J'a tent\u00e9 de r\u00e9duire la tol\u00e9rance mais le soucis c'est que c'est soit trop soit pas assez Derni\u00e8re tentative, j'ai essay\u00e9 de prendre plusieurs pixels en hauteur pour chaque incr\u00e9ment de X et en faire la moyenne, et m\u00eame comme ca, impossible de trouver de mani\u00e8re efficace les zones. Je pense que je vais donc revert tous mes changements pour revenir \u00e0 la version ou on les choisissait manuellement. Pas mal de temps perdu mais bon c'est comme ca ca arrive Bon j'ai fait un revert mais j'ai ajout\u00e9 une feature importante. Les zones font la largeur indiqu\u00e9e par l'utilisateur mais elles font la hauteur max comme ca toutes les window font la m\u00eame hauteur et ca permet \u00e0 l'utilisateur de ne pas forc\u00e9ment \u00eatre ultra pr\u00e9cis dans sa selection. Ce qui nous donne : \"Resultat final\" Maintenant je dirais que les deux prochaines choses \u00e0 faire seraient de stocker ces zones dans un fichier JSON ou autre pour que la calibration puisse \u00eatre envoy\u00e9e directement dans le logiciel de reconnaissance et ensuite de faire une calibration sur des images qui font la taille qu'on aura pendant les Grands Prix. Pour le moment elles sont au format 16:10 qui est le format d'\u00e9crant de mon laptop. Pour le stockage j'imagine un fichier qui donne des indications assez simples qui permettent de reconstruire le total des zones quand il est import\u00e9 plutot que d'\u00e9crire les coordonn\u00e9es en dur pour chacunes. Chaque Grande zone va impl\u00e9menter une methode qui s'occupe de mettre tous ses enfants dans un fichier. { \"MainZone\":{ \"x\": 10, \"y\": 20, \"width\": 1450, \"height\": 1340, \"DriverZone\":{ \"x\": 0, \"y\": 23, \"height\": 25, \"Windows\":[ { \"DriverPositionWindow\":{ \"x\": 0, \"y\": 0, \"width\": 35 } }, { \"DriverPositionChangesWindow\":{ \"x\": 0, \"y\": 0, \"width\":45 } } ] } } } C'est le r\u00e9sultat auquel j'aimerais arriver. Mais pour y arriver il faut encore que je cr\u00e9e les diff\u00e9rents types de window. Cela veut dire que je dois d\u00e9cider quelles informations je vais r\u00e9cup\u00e8rer de la page. Par exemple je vais conserver la position du pilote mais au final les changements de positions sont difficiles \u00e0 lire et sont redondants. Si je garde un historique des positions des pilotes je peux calculer moi m\u00eame les changements. Pareil pour gap avec la voiture devant. Je pense que je vais juste garder l'information des \u00e9carts absolus et ensuite je pourrai toujours calculer la diff\u00e9rence entre les pilotes. Ca peut para\u00eetre b\u00eate car cela rajoute du calcul mais en r\u00e9alit\u00e9 le calcul de l'OCR est extr\u00eamement gourmand alors il faut que j'\u00e9vite le plus possible d'y faire recours. Il est bien plus rapide de calculer les \u00e9carts que d'essayer de reconnaitre le texte et le convertir en chiffre. J'ai visiblement ajout\u00e9 un bug dans mon code. Maintenant tous les pilotes ont la m\u00eame image quand on les selectionne. Mais visiblement ca n'\u00e9tait pas le cas avant car j'avais pu prendres des images de chaque pilote. J'ai pass\u00e9 3 minutes \u00e0 fixer un bug stupide j'ai un peu envie de br\u00fbler ma place de travail... Mais bon au moins maintenant cela fonctionne ! Toutes les images sont r\u00e9cup\u00e8r\u00e9es et ont un format correct avec le bon nom : \"Verstappen folder\" Avec un peu de code tr\u00e8s moche j'ai pu cr\u00e9er un fichier JSON qui contient les diff\u00e9rentes infos. Cependant en exportant TOUT on se retrouve avec un fichier de 1200 lignes ce qui n'est pas optimal. Mais quand on regarde, il devrait \u00eatre possible de faire un fichier qui ne contient que les infos d'un seul pilote car ensuite il y a simplement un offset \u00e0 appliquer sur la zone et les windows. Je vais donc pouvoir commencer enfin le logiciel de d\u00e9codage qui prend en entr\u00e9e un fichier JSON comme celui ci qui a \u00e9t\u00e9 g\u00e9n\u00e8r\u00e9 avec le programme de calibration. { \"Main\": { \"x\": 40, \"y\": 230, \"width\": 1845, \"height\": 719, \"Zones\": [ { \"DriverZone\": { \"x\": 0, \"y\": 3, \"width\": 1845, \"height\": 35, \"Windows\": [ { \"Position\": { \"x\": 2, \"y\": 0, \"width\": 32 }, \"GapToLeader\": { \"x\": 204, \"y\": 0, \"width\": 96 }, \"LapTime\": { \"x\": 413, \"y\": 0, \"width\": 105 }, \"Drs\": { \"x\": 526, \"y\": 0, \"width\": 81 }, etc... } ] } } ] } } Dans le futur il faudrait ajouter d'autres choses comme par exemple les diff\u00e9rents pilotes pr\u00e9sents sur le Grand Prix et ce genre d'infos. Quoique je vais l'ajouter d\u00e9ja maintenant et plus tard je mettrai en place la feature acessible depuis l'interface. Mais le hardcoder maintenant me permet d\u00e9ja de mieux coder l'autre c\u00f4t\u00e9. Ce programme n'est en aucun cas termin\u00e9 et je vais devoir travailler encore un peu dessus pour qu'il soit utilisable correctement mais au moins il fonctionne \u00e0 peu pr\u00e8s. Exemple du json avec les noms de pilotes: { \"Main\": { \"x\": 37, \"y\": 238, \"width\": 1851, \"height\": 713, \"Zones\": [ { \"DriverZone\": { \"x\": 0, \"y\": -5, \"width\": 1851, \"height\": 35 } } ] }, \"Drivers\": [ \"Leclerc\", \"Verstappen\", etc... ] } Maintenant je vais m'attaquer au d\u00e9codage. Demain je dois finir le d\u00e9codage du JSON et je dois commencer \u00e0 impl\u00e9menter la reconnaissance des textes. Voire m\u00eame des pneus etc si j'y arrive. Mercredi 5 Avril Bon la il faut vraiment que je finisse assez vite la lecture du JSON et la reconstruction des zones pour commencer la reconnaissance. J'ai pris beaucoup de temps \u00e0 faire le programme de calibration mais je pense que c'est essentiel de prendre ce temps maintenant. (BTW il faudra quand m\u00eame retourner faire une plus jolie version par ce que la ca marche mais c'est tout) Bon apr\u00e8s pas mal de boulot je pense avoir r\u00e9ussi. Dans le nouveau programme on arrive \u00e0 r\u00e9cup\u00e8rer les diff\u00e9rentes zones : \"JSON decode result\" Un conseil de notre professeur M.Bonvin a \u00e9t\u00e9 de cr\u00e9er des Releases de versions qui ne fonctionnent pas ou pas tr\u00e8s bien. J'ai donc publi\u00e9 une premi\u00e8re release de l'OCR_TEST qui fonctionne vite fait. J'ai seulement un petit soucis, comme je recr\u00e9e compl\u00eatement la structure des driver zones avec seulement la premi\u00e8re, il y a un petit d\u00e9calage car entre les zones il y avait un gap. Ce qui fait que si la premi\u00e8re zone est parfaitement centr\u00e9e : \"Centered driver\" La vingti\u00e8me ne l'est plus exactement : \"Uncentered Driver\" Pour ca j'ai essay\u00e9 de mettre un espacement arbitraire mais c'est complexe. Je vais plut\u00f4t tenter de faire une diff\u00e9rence entre la taille de la zone compl\u00eate et de la taille additionn\u00e9e de toutes les fen\u00eatre et diviser le resultat entre toutes les fen\u00eatres. Ca n'est pas parfait mais au moins maintenant les donn\u00e9es ne touchent plus les bords de la fen\u00eatre. Et voila ! Maintenant avec le fichier de configuration en Json on arrive \u00e0 r\u00e9cup\u00e8rer toutes les infos comme si elles avaient \u00e9t\u00e9 envoy\u00e9es directement depuis l'app de calibration mais sans le processing time ! \"Verstappen folder 2 On peut donc ENFIN passer au d\u00e9codage de ces FICHUES donn\u00e9es. Je vais pouvoir impl\u00e9menter ce que j'ai fait dans le projet de test de d\u00e9codage. Gr\u00e2ce \u00e0 mon d\u00e9coupage initial qui m'a pris du temps \u00e0 impl\u00e9menter on a enfin un truc qui marche m\u00eame si je n'ai impl\u00e9ment\u00e9 que la reconnaissance de noms. \"Image reconnaissance propre\" Si on se rappelle du syst\u00e8me de window et de zones dans le diagramme plus haut, c'est assez facile de comprendre comment je m'y suis pris. En gros on des listes et des listes de listes de zones, c'est la partie un peu plus technique car il y a des zones qui peuvent contenir d'autres zones etc. Je vais commencer par la reconnaissance de noms. Voici le tableau de pilotes de 2023 \"Drivers\": [ \"Leclerc\", \"Verstappen\", \"Hamilton\", \"Alonso\", \"Russel\", \"Gasly\", \"Stroll\", \"Sainz\", \"Hulkenberg\", \"Norris\", \"Tsunoda\", \"Piastri\", \"Zhou\", \"Ocon\", \"Magnussen\", \"Perez\", \"Sargeant\", \"De Vries\", \"Bottas\", \"Albon\" ] ET voici le tableau de pilotes de 2022 : \"Drivers\": [ \"Leclerc\", \"Verstappen\", \"Sainz\", \"Perez\", \"Hamilton\", \"Russel\", \"Magnussen\", \"Gasly\", \"Ocon\", \"Alonso\", \"Tsunoda\", \"Bottas\", \"Zhou\", \"Albon\", \"Stroll\", \"Schumacher\", \"Hulkenberg\", \"Norris\", \"Latifi\", \"Ricciardo\" ] Je les notes ici car J'ai souvent besoin de changer selon le dataset que j'utilise. Dans le futur je ferai s\u00fbrement un grand dataset qui prend des pilotes de reserves et des pilotes juniors pour que dans le cas ou un pilote est remplac\u00e9 dans l'ann\u00e9e il n'y a pas besoin de tout recalibrer avec l'application. Apr\u00e8s une discussion avec M.Bonvin j'ai d\u00e9cid\u00e9 de tester 3 valeurs de convertion en noir et blanc et si je ne trouve pas un match exact je prend le nom le plus proche. Pour trouver la string la plus proche je pense que je vais utiliser quelque chose qui s'appelle la technique de Levenshtein. De ce que j'ai compris c'est un algorythme qui permet de donner une metric de diff\u00e9rence entre deux strings. Bon et \u00e9videmment il ne faut pas se tromper dans la liste des pilotes GENRE NE PAS OUBLIER QUE GEORGE RUSSELL COMPORTE DEUX WFNEWIEWV DE \"L\" A LA FIN DE SON NOM CE QUI POURRAIT ENGRANGER 2H DE DEBUGGING POUR RIEN ASK ME HOW I KNOW joker laugh J'ai vraiment un soucis avec Tsunoda, Il a trop tendeance \u00e0 le confondre avec \"TSUNDDA\" et pour des raisons obscures, quand j'applique l'algorythme de Levenshtein le plus proche n'est pas \"Tsunoda\" mais \"Sainz\" iniuvbwdiucbiubisc POURQUOI !!??!! Je pense que cela demande moins de changements de lettres enfin bon c'est quand m\u00eame pas id\u00e9al. Il va falloir que je trouve un moyen de le repond\u00e9rer. C'est dommage par ce que cela marche super avec Alonso Verstappen et Albon. J'ai un peu modifi\u00e9 la methode et j'ai fait en sorte d'envoyer tous les noms en majuscules en me disant que cela pourrait r\u00e9duire le nombre de changements. Et ca a march\u00e9 !! Cela va s\u00fbrement demander plus de tests pour \u00eatre bien s\u00fbr que tout fonctionne nikel, cependant pour le moment ca marche parfaitement avec les pilotes de 2022. Pour ce qui est de la reconnaissance de chiffres, j'ai d\u00e9ja fait une partie du boulot le premier jour alors je vais juste reprendre \u00e0 partir de l\u00e0. Je r\u00e9cup\u00e8re une string de ce type \"1:35.123\" le soucis c'est que les : se transforment parfois en . ou inversement mais bon ca devrait pas \u00eatre trop dur \u00e0 g\u00e8rer. Il faut que je transforme cette string en nombre de milisecondes (Du moins je pense que c'est le meilleur moyen pour ensuite pouvoir facilement comparer et stocker l'information). Cela fait que 1:35:123 en milisecondes donne : 1 * 1000 * 60 => 60'000 35 * 1000 => 35'000 123 => 123 Total : 60'000+35'000+123 => 95'123ms Et pour l'affichage : Minutes = ms / 60'000 secondes = (ms - (minutes/60'000))/1000 ms = ms - ((minutes 60'000) + (secondes * 1000)) Et on se retrouve avec 1:35:123 Maintenant apr\u00e8s un peu de temps pour nettoyer la string etc on se retrouve avec un r\u00e9sultat comme le suivant : Position : 0 Gap to leader : 0:0:0 Lap time : 2:15:123 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : LECLERC Sector 1 : 0:31:323 Sector 2 : 0:42:340 Sector 3 : 0:0:0 Evidemment pareil pour les autres pilotes Et je me rend compte que j'ai encore tout cass\u00e9 car le laptime ne devrait pas \u00eatre 2:15 mais 1:35... Voila apr\u00e8s une heure de debugging et des ajouts pour nettoyer les chaines on se retrouve avec : Position : 0 Gap to leader : 0:0:0 Lap time : 1:35:123 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : LECLERC Sector 1 : 0:31:323 Sector 2 : 0:42:340 Sector 3 : 0:0:0 Note: le traitement commence \u00e0 devenir long, il serait peut-\u00eatre int\u00e9ressant d'utiliser un seul Tesseract Engine ou de voir ce qui prend autant de temps, on d\u00e9passe la seconde de traitement ce qui est un peu ma limite. Apr\u00e8s on peut toujours tester de rajouter du multicore processing mais c'est pour une autre fois. Demain je m'occupe de r\u00e8gler les soucis que j'ai avec la prescision de ces temps au tour et j'\u00e9sp\u00e8re pouvoir m'occuper aussi de la position des pneus et du DRS. J'aimerais finir tout ca cette semaine. Jeudi 6 Avril Une id\u00e9e m'est pass\u00e9e par la t\u00eate pendant que je dormais, dans la liste des pilotes, quand ils sont \u00e0 plus d'un tour de retard avec le leader (Ce qui arrive normalement dans presque tous les Grand Prix) on a pas des minutes mais une string qui montre \"+1 Lap\" ou \"+2Laps\" ce qui est \u00e9videmment un probl\u00e8me. Je pense qu'une bonne facon d'envoyer l'info serait de retourner -1 -2etc... \u00e0 la place des milisecondes, mais encore faut-il detecter le nombre de tours Je devrais \u00eatre en train de commencer la documentation de commment tout ce que j'ai fait fonctionne. Cependant je ne me vois pas faire ca tant que je n'ai pas au moins r\u00e9cup\u00e8r\u00e9 toutes les infos au moins un peu proprement. Cela veut dire que je commence officiellement \u00e0 prendre du retard. (Sachant que si je finis tout aujourd'hui une journ\u00e9e de doc suffira largement le terme est un peu exag\u00e8r\u00e9 mais bon) Bon pour la reconnaissance des temps c'est sp\u00e9cial... Le filtre semble ne pas changer grand chose ce qui est probl\u00e9matique et ca n'est vraiment pas fiable. Voici quelques expemples avec un treshold de 100: \"11ZSD\" Cette image est comprise comme \"11ZSD\" 42340 Cette image est comprise comme \"42340\" \"ZZAEB\" Et celle ci \"ZZAEB\"... Ce qui... n'est pas bon du tout... J'ai essay\u00e9 de trouver un fichier d'entrainement sp\u00e9cifiquement fait pour les digits. J'ai essay\u00e9 de blacklister les chars non voulus pour tenter d'obliger Tesseract \u00e0 trouver des chiffres. Avec la premi\u00e8re option, les r\u00e9sultats ne sont pas meilleurs voire pires. Avec la seconde option c'est d\u00e9ja pas mal mieux mais on perd compl\u00e8tement la possibilit\u00e9 de detecter les mots comme \"LEADER\" ou \"LAP\" et de toute facon ca n'est pas parfait. Le soucis c'est que si je n'ai pas des donn\u00e9es fiables c'est juste impossible de faire des calculs et de l'affichage correct... Il faut absolument que je trouve une solution. J'ai essay\u00e9 d'utiliser de l'interpolation our augmenter la taille de l'image et ensuite appliquer mon filtre pour retirer le flou mais sans succ\u00e8s... Pourtant la on se retrouve avec des images plut\u00f4t claires : \"Clear1\" Ici le programme trouve \"44301\" \"Clear2\" Et ici \"A5151\"... On a toujours les m\u00eames probl\u00e8mes. Bon je suis all\u00e9 me renseigner sur l'OCR et je me suis dit que j'allais tenter de faire les choses proprement. Je vais faire passer plusieurs \u00e9tapes de postProcessing avant de donner l'image \u00e0 Tesseract. GrayScale Tresholding InvertColors Scaling Dilatation Ce qui donne : \"Original\" \"Grayscale\" \"InvertColors\" ; \"Resize\" ; \"Dilatation\" Ce qui ne change : Roulement de tambour RIEN kjd viuwvuirnvoirenbf Tout ca pour rien... C'EST BON !!! Bon en fait au final le probl\u00e8me \u00e9tait une mauvaise configuration de Tesseract. Je vais devoir un peu nettoyer tout ca. Mais avec les changements de l'image on a des r\u00e9sultats BEAUCOUP plus pr\u00e9cis et potentiellement utilisables. La je vais devoir faire un serieux travail de nettoyage et simplification de mon code par ce que la c'est vraiment un chantier vu le nombre de choses que j'ai du essayer. J'ai du aussi beaucoup modifier la gestion de l'image ce qui donne : \"Clean\" Et la on a des r\u00e9sultats qui sont vraiment bons. J'ai pu ajouter assez facilement la detection de position comme c'est simplement un chiffre. On se retrouve maintenant avec ce genre de retours : Position : 1 Gap to leader : 8:33:51 Lap time : 2:19:123 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : LECLERC Sector 1 : 0:31:828 Sector 2 : 0:42:940 Sector 3 : 0:0:0 Position : 2 Gap to leader : 0:3:259 Lap time : 23:12:392 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : VERSTAPPEN Sector 1 : 0:38:119 Sector 2 : 0:0:0 Sector 3 : 0:0:0 Il ne manque plus que l'impl\u00e9mentation de la reconnaissance du DRS et des Pneus Et non... je viens de me rendre compte que mon programme a encore cass\u00e9 car le tap time ne peut pas \u00eatre 23 min lol. J'ai un nouveau magnifique probl\u00e8me... Les points et les deux points sont interpr\u00eat\u00e9s comme des chiffres ... Give me a F * * break... J'ai du mal \u00e0 comprendre pourquoi ils ne sont d\u00e9tect\u00e9s comme tels que maintenant. Bon alors il semblerait les temps au tour aie besoin d'un ordre tr\u00e8s pr\u00e9cis pour fonctionner. Grayscale InvertColors Tresholding Resize * 2 Resize * 2 Et la on a des r\u00e9sultats un peu mieux. Bon demain il faut absolument que je me charge de r\u00e8gler tous ces probl\u00e8mes et que je commence la reconnaissance des pneus et de DRS par ce que je commence \u00e0 \u00eatre en retard. Vendredi 6 Avril 2023 Alors aujourd'hui c'est le dernier jour avant de commencer \u00e0 \u00eatre en retard pour de bon. J'ai r\u00e9ussi \u00e0 r\u00e8gler le probl\u00e8me des temps au tour, des gaps, et des secteurs. Dans le processus j'ai cass\u00e9 la detection de position mais ca devrait pas \u00eatre TROP compliqu\u00e9. Et voila ... Apr\u00e8s seulement plus de dix heures de gal\u00e8re, si on donne cette image au programme et le bon JSON le programme nous retourne : Position : 1 Gap to leader : 0:05:059 Lap time : 1:39:123 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : LECLERC Sector 1 : 0:31:828 Sector 2 : 0:42:940 Sector 3 : 0:00:000 Position : 2 Gap to leader : 0:03:259 Lap time : 1:39:392 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : VERSTAPPEN Sector 1 : 0:31:749 Sector 2 : 0:00:000 Sector 3 : 0:00:000 Evidemment le GapToLeader est faux sur leclerc car il est leader mais bon ca je pourrai toujours Hardcoder que le premier a jamais de GapToLeader. Bon j'ai eu beaucoup de soucis que je ne vais pas mentionner ici car ce sont simplement des soucis de logique de programmation pour trouver un DRS ouvert ou non. Au final la technique que j'utilise et qui marche plut\u00f4t bien pour le DRS est que je prend la premi\u00e8re image de DRS et je la d\u00e9clare comme valeur \u00e9talon d'un DRS non actif, en effet dans 99% des cas le leader n'a pas de DRS (cela peut arriver alors il faudra donc juste verifier que les pilotes sont bien \u00e0 moins de deux secondes les uns des autres pour confirmer). Ensuite cette valeur \u00e9talon je la calcule en fonction du nombre de pixels verts dans l'image et si il y a plus de 30% de pixels verts en plus c'est que le DRS est activ\u00e9 ex: Ceci est un DRS ferm\u00e9: \"Closed DRS\" Ceci est un DRS ouvert: \"Open DRS\" Cela marche \u00e0 peu pr\u00e8s tout le temps mais dans le pire des cas on peut toujours verifier que les pilotes sont bien proches pour detecter les potentiels rares cas de faux positifs. J'ai pu augmenter les performances en utilisant un seul engine pour tout le monde et en arr\u00eatant d'utiliser GetPixel et SetPixel qui sont simplement des horreurs \u00e0 utiliser. Mais elles ne sont pas encore bonnes Le soucis avec la detection de pneus cependant, c'est qu'il n'est pas possible d'utiliser la reconnaissance pour savoir ou regarder la couleur car cela ne marcherait pas. Je ne peux pas faire trop de post processing car je dois conserver la couleur Je ne peux pas hardocder un endroit ou aller regarder car cela \u00e9volue tout le long du Grand Prix. Bref c'est la gal\u00e8re. En y r\u00e9flechissant je me suis dit qu'une bonne id\u00e9e pourrait \u00eatre de partir de la droite de la zone du pneu en regardant au milieu de la hauteur. Puis continuer vers la gauche jusqu'\u00e0 ce que je rencontre une couleur diff\u00e9rente. Je pourrai ensuite faire une zone un peu vers la gauche qui devrait contenir les infos du pneu et sur laquelle il sera possible de faire de le reconnaissance de couleur et de la reconnaissance de chiffres. J'ai d\u00e9termin\u00e9 que le background n'\u00e9tait jamais plus clair que #505050 et que donc nimporte quelle couleur qui aurait plus que 50 dans un seul des channels serait consid\u00e8r\u00e9e comme une couleur cassant le background Pour arriver \u00e0 cette conclusion je me suis amus\u00e9 un peu avec les couleurs pour jouer avec les limites de mon algorythme : \"Color fun\" Et je crois que j'ai eu une bonne id\u00e9e, avec une petite methode bien faite on arrive \u00e0 de supers r\u00e9sultats : private Rectangle FindTyreZone() { Bitmap bmp = WindowImage; int currentPosition = bmp.Width; int height = bmp.Height / 2; Color limitColor = Color.FromArgb(0x50,0x50,0x50); Color currentColor = Color.FromArgb(0,0,0); Size newWindowSize = new Size(bmp.Height,bmp.Height); while(currentColor.R <= limitColor.R && currentColor.G <= limitColor.G && currentColor.B <= limitColor.B && currentPosition > 0) { currentPosition--; currentColor = bmp.GetPixel(currentPosition,height); } //Its here to let the new window include a little bit of the right side int offset = Convert.ToInt32((float)newWindowSize.Width / 100f * 20f); int CorrectedX = currentPosition - (newWindowSize.Width - offset); if (CorrectedX <= 0) return new Rectangle(0,0,newWindowSize.Width,newWindowSize.Height); return new Rectangle(CorrectedX,0,newWindowSize.Width,newWindowSize.Height); } \"Tyres\" Maintenant cela devrait \u00eatre beaucoup plus simple de trouver la couleur g\u00e9n\u00e9rale et le nombre de tours. Donc ce que je fais c'est que je fais une reconnaissance de texte sur l'image r\u00e9duite. Si je trouve une lettre c'est facile Ca me donne le type de pneu et ca me dit que c'est le premier tour avec. Si c'est un nombre alors je fais la moyenne de toutes les couleurs de l'image et je prend la couleur de pneu la plus proche. Voici les diff\u00e9rentes couleurs de pneus : SOFT : #FF0000 MEDIUM : #f5bf00 HARD : #d9d8d4 INTER : #00a42e WET : #2760a6 \"Tyre colors\" Les couleurs de pneus peuvent changer de temps \u00e0 autres, par exemple cette r\u00e8gle de pneus est arriv\u00e9e en 2019 et avant il y avait beaucoup plus de couleurs mais dans une volont\u00e9 de rendre le sport plus facile \u00e0 comprendre \u00e0 la t\u00e9l\u00e9 cela a \u00e9t\u00e9 simplifi\u00e9. Je ne pense pas que cela va changer dans les ann\u00e9es qui viennent alors tout est hardcod\u00e9. Je pense que j'ai des soucis avec la detection de texte et de couleur car ma zone est trop grande. Alors bon j'\u00e9crit ces lignes apres des heures de tests. Il semble que la principale difficult\u00e9 avec ces pneus c'est que les chiffres ou lettres sont minuscules. Il est donc extr\u00eamement difficile de faire une reconnaissance ne serait-ce qu'un peu fiable.. Je fais de mon mieux pour tenter de r\u00e8gler le soucis cependant c'est vraiment complexe. Je commence \u00e0 devenir fou, je tente tout et nimporte quoi pour permettre \u00e0 mon algo de fonctionner et m\u00eame quand je fais du post processing comme pas possible il me retourne toujours nimporte quoi... \"5i t'inqui\u00e8tes\" Ici le programme va trouver '5i'... En fait c'est complexe d'expliquer tout ce que je fais car je change tout en boucle en essayant et en ratant ce qui prend des heures. Pour aujourd'hui j'abandonne je vais simplement rentrer chez moi et y r\u00e9flechir cette nuit mais je ne vois pas comment mieux faire la... C'est terrible par ce que je sens que je ne suis pas bien loin. Vacances Bon je vais un peu laisser de c\u00f4t\u00e9 la detection de chiffres pour me pencher un peu plus sur la d\u00e9tection de couleur. Par ce que techniquement si j'arrive \u00e0 toujours parfaitement la detecter alors je pourrais me passer des chiffres car ils sont redondant si je construit un historique de pneus. J'ai r\u00e9ussi \u00e0 fix mon probl\u00e8me de mauvaise detection de couleur de pneus. Du moins je crois. Seulement j'ai quand m\u00eame un souci, les fen\u00eatres de pneus avec une lettre n'ont pas assez de couleur pour \u00eatre d\u00e9tect\u00e9s. Je vais donc essayer de detecter les cinq lettres possibles et si il ne trouve pas alors je pourrai tenter de detecter les chiffres sans lettres ce qui devrait grandement aider. Le but est encore une fois de r\u00e9duire les possibilit\u00e9s de Tesseract. Je me rend de plus en plus compte que le plus important c'est de r\u00e9duire le scope le plus possible. Moins il y a de mots et lettres et de chiffres possibles meilleure sera la reconnaissance. Bon ca ne veut toujours pas marcher maintenant le 11 est interpr\u00eat\u00e9 comme trois I ou comme un M... J'en ai marre sans rire c'est vraiment p\u00e9nible. Alors j'\u00e9crit ces lignes deux jours plus tard et me rend compte avec horreur que toutes mes modifications sur ce journal de bord n'ont pas \u00e9t\u00e9 auvegard\u00e9e... yess.. Bon pour faire simple, j'ai r\u00e9ussi \u00e0 rendre la detection de couleurs bien plus efficace en r\u00e9duisant la taille de l'image et en ne prenant pas en compte les couleurs que l'on d\u00e9tecte comme \u00e9tant partie int\u00e9grante du background. Par exemple quand on a une image comme celle ci : \"Avec background\" qui contient un background alors que ci dessous, on l'a enlev\u00e9. \"Sans background\" La diff\u00e9rence est t\u00e9nue mais elle permet de grandement am\u00e9liorer la prescision de la reconnaissance de couleurs. Pour ce qui est du nombre de tours je me suis rendu compte que cela n'\u00e9tait d\u00e9ja pas tr\u00e8s utile car avec l'historique on devrait pouvoir le d\u00e9duire. Mais bon pour la forme je me suis dit que cela serait quand m\u00eame une bonne id\u00e9e de v\u00e9rifier avec la reconnaissance. J'\u00e9tais quasi certain que le soucis \u00e9tait le fait que l'on voie le contour du logo de pneu qui faisait que la reconnaissance avait du mal. Et j'avais raison ! En les enlevant (Ce qui n'a pas \u00e9t\u00e9 simple) J'ai pu avoir des chiffres beaucoup plus proches de la r\u00e9alit\u00e9. En m\u00eame temps je ne vois pas bien comment j'aurais pu faire mieux : \"Super 11\" Je suis quand m\u00eame assez fier de voir que j'ai r\u00e9ussi \u00e0 part de l'image que on peut voir un peu plus haut et automatiquement la transormer en celle ci-dessus. J'ai donc pu retirer le round autour du chiffre et cela m'a permit de pouvoir d\u00e9zoomer un peu et c'est avec ca que les lettres ont pu \u00eatre mieux reconnues : \"Super H\" \"Super M\" Maintenant je pense qu'il ne reste \"plus qu'\u00e0\" nettoyer un peu tout ce code qui traine de partout pour tout faire fonctionner et impl\u00e9menter un peu de parrallel processing ainsi que de l'asynchrone pour ne pas bloquer le reste du programme. Par ce qu'il faut savoir que en l'\u00e9t\u00e2t, le programme met 25 secondes \u00e0 d\u00e9marrer et consomme presque 2GB de Ram. Certes cela ne veut pas dire que la reconnaissance \u00e0 elle seule prend 25 secondes car au d\u00e9marrage il y a aussi la lecture du fichier de config et la cr\u00e9ation des window etc.. En r\u00e9alit\u00e9 la partie strictement OCR prend dans les 12s si on en croit la fonction stopWatch de C#. Et quand on change d'image la reconnaissance prend 9s. Dans tout les cas c'est BEAUCOUP trop. J'aurais eu comme objectif de faire une reconnaissance toutes les secondes. Je ne sais pas bien si cela va \u00eatre possible mais en tout cas le but va \u00eatre de s'en rapprocher. Pour \u00eatre plus exact et permettre une comparaison, voici les stats exactes Avec un fichier d'images vide : Loading - 11.8s Splitting d'images - 90ms OCR - 12.5s Avec un fichier d'images plein : Loading - 10.8s Splitting d'images - 80ms Ocr - 11.6s En passant d'une image \u00e0 l'autre : Loading - NaN Splitting d'images - 50ms Ocr - 8.8s Donc on peut voir que les deux endroits ou le programme prend le plus de temps c'est au premier d\u00e9marrage quand il faut lire le fichier et setup les windows etc... Et l'OCR qui prend un temps fou. Ce qui est pratique c'est que les presque 2gb de ram sont utilis\u00e9s que au lancement et ensuite l'application n'en utilise que quelques centaines de mb. Le processeur lui tourne entre 10 et 20% ce qui ne va pas durer :) Je vais m'occuper dabord du loading. J'ai essay\u00e9 d'utiliser un Parrallel.For au moment de la cr\u00e9ation des windows, le probl\u00e8me c'est que visiblement les objets windows sont beaucoup trops complexes et utilisent trop de ressources partag\u00e9es pour \u00eatre vraiment thread Safe. J'\u00e9sp\u00e8re que je n'aurais pas trop de soucis avec ca qu'en j'en viendrai \u00e0 l'optimisation de l'OCR... Ce qui me rend fou c'est que cette boucle toute nulle prend plus de dix secondes \u00e0 s'executer et je ne comprend pas bien pourquoi. for (int i = 0; i < NUMBER_OF_DRIVERS; i++) { Point tmpPos = new Point(0, FirstZonePosition.Y + i * FirstZoneSize.Height - Convert.ToInt32(i * offset) /*- (i* (FirstZoneSize.Height / 32))*/); Zone newDriverZone = new Zone(MainZoneImage, new Rectangle(tmpPos, FirstZoneSize)); Bitmap zoneImg = newDriverZone.ZoneImage; newDriverZone.AddWindow(new DriverPositionWindow(zoneImg, new Rectangle(driverPositionPosition, driverPositionArea))); newDriverZone.AddWindow(new DriverGapToLeaderWindow(zoneImg, new Rectangle(driverGapToLeaderPosition, driverGapToLeaderArea))); newDriverZone.AddWindow(new DriverLapTimeWindow(zoneImg, new Rectangle(driverLapTimePosition, driverLapTimeArea))); newDriverZone.AddWindow(new DriverDrsWindow(zoneImg, new Rectangle(driverDrsPosition, driverDrsArea))); newDriverZone.AddWindow(new DriverTyresWindow(zoneImg, new Rectangle(driverTyresPosition, driverTyresArea))); newDriverZone.AddWindow(new DriverNameWindow(zoneImg, new Rectangle(driverNamePosition, driverNameArea))); newDriverZone.AddWindow(new DriverSector1Window(zoneImg, new Rectangle(driverSector1Position, driverSector1Area))); newDriverZone.AddWindow(new DriverSector2Window(zoneImg, new Rectangle(driverSector2Position, driverSector2Area))); newDriverZone.AddWindow(new DriverSector3Window(zoneImg, new Rectangle(driverSector3Position, driverSector3Area))); MainZone.AddZone(newDriverZone); } Alors que Zone.AddWindow c'est simplement : public virtual void AddWindow(Window window) { Windows.Add(window); } Et windows est simplement une liste. Donc ca ne peut pas \u00eatre ca qui prend du temps. Et les windows que je cr\u00e9\u00e9 ont ca comme code : public DriverPositionWindow(Bitmap image, Rectangle bounds) : base(image, bounds) { Name = \"Position\"; } Sachant que le constructeur de base d'une Window c'est : public Window(Bitmap image, Rectangle bounds) { Image = image; Bounds = bounds; Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, \"eng\", EngineMode.Default); Engine.DefaultPageSegMode = PageSegMode.SingleLine; } Sachant que TesseractEngine est en statique et que donc il ne devrait... OHLLALALALALALALALALA je suis un imb\u00e9cile... J'ai juste \u00e0 changer ce constructeur avec ca: if (Engine == null) { Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, \"eng\", EngineMode.Default); Engine.DefaultPageSegMode = PageSegMode.SingleLine; } ET le loading ne prend plus que 2-300 ms... Bon c'est une tr\u00e8s belle am\u00e9lioration pour pas tr\u00e8s ch\u00e8r mais bon c'est un peu b\u00eate... Bon je pense que 2-300ms c'est une dur\u00e9e correcte surtout que ca n'est appel\u00e9 qu'une fois pour le lancement. On peut passer \u00e0 la suite maintenant. Alors il y a un grand soucis avec la parallellisation de l'OCR... Tesseract n'est pas par d\u00e9faut une classe \"Thread safe\" ce qui veut dire que je ne peut utiliser de parallell.Foreach sur mes windows pour acc\u00e8l\u00e8rer le traitement drastiquement. Je pourrais par exemple avoir une instance de Tesseract par window sauf que cela fait 20 pilotes * 9 windows chacuns ce qui donne 180 instances ce qui n'est tout simplement pas raisonnable. Je vais donc essayer de voir avec l'utilisation de methodes asynchrones qui me permettraient de faire un genre de flux tendu de reconnaissance. J'avoue que la je navigue un peu \u00e0 vue, je me base sur diff\u00e9rentes infos que je trouve sur des sites un peu perdus et sur chatGPT, j'esp\u00e8re que j'arriverai \u00e0 trouver une solution car 10 secondes de reconnaissance c'est vraiment beaucoup trop. Alors le soucis avec un Engine unique entre toutes les windows c'est qu'il n'est pas possible de process plusieurs images \u00e0 la fois. Je vais donc retirer l'engine unique pour voir si en cr\u00e9er un par window me permet de passer en multithreading. La grande question sera : Est-ce que les ressources suppl\u00e9mentaires que vont prendre la cr\u00e9ation de tous ces engines va compenser enti\u00e8rement le temps gagn\u00e9 avec la paralellisation. Pour stocker les donn\u00e9es dans un premier temps je vais cr\u00e9er un objet DriverData. Ce qu'il y a de pratique avec ca, c'est que je pourrais ajouter du code de v\u00e9rification de certaines donn\u00e9es directement dedans avant de les donner \u00e0 la suite du programme. Et on peut m\u00eame imaginer une impl\u00e9mentation d'une liste de DriverData pour avoir l'historique. Ce qui serait cool ca serait de grouper toutes ces data avec un num\u00e9ro de tour. Placer ensuite la liste de Data dans une DB serait ainsi super simple. Mais il va falloir savoir quoi mettre, quelles infos sont redondantes et prendre en compte le fait que un tour affich\u00e9 sur la page de la F1TV n'est accompli que par certains des premiers pilotes. D'autres pilotes peuvent \u00eatre dans des tours pr\u00e9c\u00e9dents si ils ont du retard. Il faudra r\u00e9fl\u00e9chir \u00e0 cela quand je viendrai au mod\u00e8le. Bon pour y arriver j'ai du faire de gros changements et le r\u00e9sultat n'est peut-\u00eatre pas aussi cool que ce que j'aurais voulut... Voici un petit point sur les performances maintenant J'ai \u00e9galement d\u00e9sactiv\u00e9 le dump d'images. Pour le moment j'ai tout mis en commentaire mais cela pourrait \u00eatre int\u00e9ressant de faire en sorte de pouvoir l'activer en changeant une ou deux variables Au d\u00e9marrage : Loading - 113ms Splitting d'images - 14ms Ocr - 7s En passant d'une image \u00e0 l'autre : Loading - 113ms Splitting d'images - 13ms Ocr - 5s Alors clairement les stats montrent qu'il y a eu un changement mesurable mais bon je pensais pouvoir en gagner un peu plus... Je soupconne la cr\u00e9ation d'engines d'\u00eatre \u00e0 l'origine de ces performances presque d\u00e9cevantes. Autre soucis, il semble que plus je change d'image plus la detection est lente et plus je consomme de RAM. Il va falloir que je travaille encore un peu. J'ai tent\u00e9 de mettre un stopwatch sur une des cr\u00e9ations d'engine Tesseract et le r\u00e9sultat me parait fou... Plus d'une seconde c'est dingue. J'ai test\u00e9 dans d'autres endroits du code et effectivement il semble que la cr\u00e9ation d'un engine prenne entre une et deux secondes ce qui est une ETERNITEE what ! Donc il faut optimiser tout ca. Une id\u00e9e serait de d\u00e9composer le threading mais cela me demanderait un gros refactor et je n'ai pas envie d'en refaire un la... Sinon, une fois qu'ils sont cr\u00e9\u00e9s ils ne prennent pas de temps du tout. Cr\u00e9er une fois tous les engines et ensuite les utiliser pourrait \u00eatre une bonne id\u00e9e. Cela prendrait longtemps au load mais ensuite les reconnaissances devraient \u00eatre super rapides. Ok alors ca c'est d\u00e9ja plus ce \u00e0 quoi je m'attendais ! On est de nouveau \u00e0 plus de 10s de loading time mais on est descendu \u00e0 deux secondes par OCR. (Bon autre soucis, l'utilisation de la RAM est ridicule plus de 2gb mais ce qui m'inqui\u00e8te c'est que j'ai l'impression qu'elle augmente plus on change d'image) J'ai r\u00e8gl\u00e9 (en partie) le soucis en obligeant le GC (Garbage Collector) \u00e0 collecter apr\u00e8s chaque detection. m\u00eame apr\u00e8s 50 detections l'utilisation de la ram se stabilize autour des 2GB. Bon en paralellisant la cr\u00e9ation des Engines le soucis c'est que cela demande d'allouer beaucoup trop de m\u00e9moire d'un coup alors le programme se fige pendant genre cinq secondes avant de tout cr\u00e9er. Du coup m\u00eame si la cr\u00e9ation est plus rapide, on se retrouve avec un temps total plus long... Je pense que l'on va devoir se contenter de ces dix secondes. Bon la j'allais tenter de faire la documentation mais je viens de me rendre compte que la detection de temps au tour est pas vraiment encore id\u00e9ale... J'ai r\u00e9ussi \u00e0 changer un petit peu le programme de reconnaissance pour rendre la reconnaissance un peu meilleure mais cela a drastiquement augment\u00e9 le temps requis pour d\u00e9coder... On arrive \u00e0 3.5 secondes. Je vais tenter de rajouter un peu de parralell processing sur les boucles de traitement voir si cela peut aider. Alors effectivement cela aide pas mal, on arrive maintenant \u00e0 faire une detection presque tout le temps en dessous de la seconde. Et j'ai aussi du changer un peu le fonctionnement de la detection des Temps au tour. Et voila je pense que je vais m'arr\u00eater la pour la partie d\u00e9codage. Je ne pense pas que je peux facilement faire mieux que ca et il faut que j'avance dans d'autres parties du projet. Je vais pouvoir commencer \u00e0 documenter un peu toute la partie OCR. Il faut que je prenne le temps de le faire bien car c'est la partie la plus int\u00e9ressante du projet et ou je pense que j'aurai le plus essay\u00e9 de choses qui vallent le coup d\u00eatres racont\u00e9es. J'ai aussi pass\u00e9 pas mal de temps sur le poster du projet. J'avais fait des croquis au crayon de ce \u00e0 quoi je pensais, cependant apr\u00e8s de longues discussions avec M.Garcia ils n'\u00e9taient pas forc\u00e9ment tr\u00e8s bons car ils ne repr\u00e9sentent pas assez bien le fonctionnement du projet et sont un peu trop marketings. Du coup j'ai fait une premi\u00e8re version au propre : \"Poster V1\" Mais je n'\u00e9tais pas forc\u00e9ment content du r\u00e9sultat et il manquait des choses je trouve comme par exemple l'utilisation de Selenium. J'ai donc repass\u00e9 des heures \u00e0 faire une seconde version : \"Poster V2\" La police d'\u00e9criture n'est pas encore la bonne mais cela va venir. Mais je pr\u00e9f\u00e8re d\u00e9ja beaucoup cette version \u00e0 la premi\u00e8re. Je ne sais pas encore si la version finale sera une version plus travaill\u00e9e de ce poster ou compl\u00eatement autre chose mais pour l'instant je suis \u00e0 peu pr\u00e8s content de cette version. Je le trouve un tout petit peu trop brouillon ou avec trop d'infos mais il m'a \u00e9t\u00e9 de nombreuses fois reproch\u00e9 de ne pas assez montrer le fonctionnememt interne et je ne peux pas faire plus simple. L'ajout des nombres pour compartimenter le projet ajoute de la structure mais je me demande si cela suffit. Maintenant que je suis \u00e0 peu pr\u00e8s content de mon code pour l'OCR je vais commencer sa documentation. (Uniquement son fonctionnement interne pas comment s'en servir car cela va changer) Bon j'ai cr\u00e9\u00e9 u nouveau projet selenium mais m\u00eame avec les bonnes libraries je n'arrivais pas \u00e0 faire fonctionner firefox j'avais toujours une erreur \"OpenQA.Selenium.WebDriverException: 'Cannot start the driver service on http://localhost:51481/'\" et j'ai pu r\u00e8gler le probl\u00e8me en t\u00e9l\u00e9chargeant directement le gecko driver depuis le git https://github.com/mozilla/geckodriver/releases et utiliser le fichier directement dans le service : var service = FirefoxDriverService.CreateDefaultService(AppDomain.CurrentDomain.BaseDirectory+@\"geckodriver-v0.27.0-win32\\geckodriver.exe\"); FirefoxOptions options = new FirefoxOptions(); var driver = new FirefoxDriver(service,options); Le seul probl\u00e8me c'est que du coup il faut tout le temps d\u00e9placer le fichier dans le dossier bin si je clone le projet. Il faudra faire un installeur dans la version finale qui s'occupe de tout je pense. Je me suis dit que j'allais garder la doc pour le retour des vacances quand j'aurai un bureau un clavier et un setup complet un peu propres. Bon il va falloir que je parle de la r\u00e9cup\u00e9ration de cookie. J'ai d\u00e9ja pu travailler lors d'un poc sur la meilleure facon de prendres des screenshots de la F1TV : Avoir une page chrome ouverte avec le feed en plein \u00e9cran et un programme qui prend des captures d'\u00e9crans. Avoir une cam\u00e9ra qui prend en photo l'\u00e9cran au cas ou chrome et Firefox emp\u00eachent la prise de captures d'\u00e9crans. R\u00e9cup\u00e8rer directement le feed en faisant du reverse engeneering de la plateforme. Simuler un chrome en background qui prenne des screenshot sans qu'on aie \u00e0 le voir. Dans toutes ces options, je dirais que la pire \u00e9tait celle de la cam\u00e9ra qui filme l'\u00e9cran, mais \u00e0 l'\u00e9poque c'\u00e9tait encore envisageable comme solution de dernier recours. Le soucis de cette solution c'est l'horreur que serait la partie OCR avec une image de tr\u00e8s mauvaise qualit\u00e9. Une autre option qui m'aurait vraiment emb\u00eat\u00e9 aurait \u00e9t\u00e9 de devoir garder une page de Chrome ou Firefox ouverte quelque part sur un \u00e9cran pour que le programme puisse prendres des captures d'\u00e9crans. C'est de loin l'option la plus simple et la plus logique mais elle poss\u00e8de pour moi de tr\u00e8s gros points noirs : On ne peut pas certifier l'int\u00e9grit\u00e9 des donn\u00e9es car l'utilisateur a le contr\u00f4le total sur le feed. Il peut mettre pause, avancer, reculer, tout casser sans faire expr\u00e8s en ouvrant autre chose sur son ordi qui se mette pile devant. Bref c'est un peu bancale. Et surtout on bloque une partie non significative de l'\u00e9cran de l'utilisateur avec des infos redondantes. Et je peux vous dire que quand je commente la F1 j'ai besoin de beaucoup d'informations et que chaque centim\u00e8tre d'\u00e9cran est crucia\u00e9 ! Alors avoir un \u00e9cran complet bloqu\u00e9 est juste un point bloquant qui m'emp\u00eacherait d'utiliser l'app aussi bonne soit-elle dans ses pr\u00e9dictions. Mais bon si aucune autre methode ne fonctionne ce qui est bien c'est que celle la est plut\u00f4t simple \u00e0 mettre en place. Ensuite reverse engeneer le feed serait l'option la plus classe, cependant c'est la plus complexe et la plus bancale au niveau l\u00e9gal haha. L'id\u00e9e serait de r\u00e9cup\u00e8rer le lien vers le broadcast g\u00e9n\u00e9ral et de comprendre comment il fonctionne pour le d\u00e9coder nous m\u00eame pendant un Grand Prix. Seuls soucis : Il n'est pas possible de faire des tests en dehors des periodes de Grand Prix (Et je rappelle que c'est des p\u00e9riodes ou je travaille en plus) Difficile de faire un syst\u00e8me qui marche pareil pour les rediffusions et les lives. (En effet les liens des rediff sont beaucoup plus simple \u00e0 r\u00e9cup\u00e8rer mais ne fonctionnent pas du tout pareil et pour tester l'app il est essentiel de pouvoir s'entrainer sur des anciens Grand Prix) Dernier GROS soucis, je ne sais tout simplement pas faire ca lol. Je ne sais pas comment faire. Peut-\u00eatre que avec des profs qui m'aident et chat gpt ainsi qu'internet je pourrais potentiellement n\u00e9gocier un truc mais c'est hautement improbable et cela serait une perte de temps folle si je n'y arrive pas. Derni\u00e8re option que je trouve la plus s\u00e9duisante. Simuler une instance de Chrome ou de Firefox (Le soucis avec chrome c'est qu'il impl\u00e9mente l'utilisation de DRM dans les vid\u00e9os qui fait qu'il est tr\u00e8s difficile de passer outre la s\u00e9curit\u00e9 avec un bot) pour ensuite prendre des captures d'\u00e9crans automatiquement. Cette solutions offre pleins d'avantages : Pas de place prise sur l'\u00e9cran L'int\u00e9grit\u00e9 des donn\u00e9es est assur\u00e9e car c'est le programme qui d\u00e9cide d'ou partir et de si il met pause ou non C'est une option complexe mais beaucoup moins que le reverse engeneering Elle permet de ne demander presque aucun input de la part de l'utilisateur. Mais elle pose quelques probl\u00e9matiques : Comment se connecter automatiquement sans \u00eatre detect\u00e9 par un Bot et sans demander \u00e0 l'utilisateur ses identifiants (Pour des raisons \u00e9videntes qui sont : QUI VA METTRE SES IDENTIFIANTS SUR UNE VIEILLE APP COMME LA MIENNE??) Comment faire en sorte que le programme prenne les meilleures captures dans la meilleure qualit\u00e9 et en plein \u00e9cran. Mais j'ai d\u00e9cid\u00e9 de partir sur cette option. Pour ce faire j'utilise Selenium. J'ai pu tester Puppetteer Sharp et m\u00eame si dans un premier temps j'ai pu avancer asez vite, malheureusement il y a des bugs qui rendent son utilisation impossible dans notre contexte. J'ai donc d\u00e9cid\u00e9 de tout faire en utilisant un portage de Selenium dans mon programme. Voici un exemple de code qui va ouvrir FireFox et qui va lancer un RickRoll var service = FirefoxDriverService.CreateDefaultService(AppDomain.CurrentDomain.BaseDirectory+@\"geckodriver-v0.27.0-win32\\geckodriver.exe\"); service.Host = \"127.0.0.1\"; service.Port = 5555; FirefoxOptions options = new FirefoxOptions(); options.AddArgument(\"--disable-headless\"); var driver = new FirefoxDriver(service,options); driver.Navigate().GoToUrl(\"https://www.youtube.com/watch?v=dQw4w9WgXcQ&autoplay=1&mute=1\"); Dans cet exemple on d\u00e9sactive le \"Headless\" pour qu'on puisse voir ce que fait l'app car sinon tout est invisible. Alors dans les faits la vid\u00e9o youtube ne se lance pas du tout car il y a des pubs et des prompts de cookies que l'on doit accepter etc... ce qui montre les diff\u00e9rents challenges que l'on va devoir surmonter pour vraiment faire ce que l'on veut. Mais un petit d\u00e9tail extr\u00eamement important, la F1TV est un programme payant un peu comme netflix. Ce qui veut dire que pour acc\u00e8der au contenu il faut \u00eatre connect\u00e9. Sauf que une instance de firefox cr\u00e9\u00e9 par Selenium est comme une page de naviguation priv\u00e9e, ce qui veut dire que si on va sur la page de la F1TV on est pas connect\u00e9s. Je pourrais tout \u00e0 fait demander \u00e0 l'utilisateur de me donner ses identifiants pour que j'aille ensuite automatiquement me connecter sauf que cela pose deux soucis: Personne ne voudra mettre ses identifiants sur mon programme La page de login de la F1TV a \u00e9t\u00e9 prot\u00e8g\u00e9e avec la meilleure technologie de detection de bots que je connaisse. Presque aucun site n'arrive \u00e0 me detecter sauf eux ! Donc c'est tout simplement impossible d'utiliser cette technique. Ensuite je me suis rappel\u00e9 que ce que la page stocke pour me permettre de rester connect\u00e9 ce sont des cookies. Et si je mets le bon cookies dans Selenium alors je serai connect\u00e9. Dans un premier temps je voulais faire un syst\u00e8me ou l'utilisateur irait prendre dans son chrome son cookie et le copie colle dans mon programme mais c'est immonde. C'est alors que vient la partie r\u00e9cup\u00e8ration de cookies ! Tous les cookies de chrome sont stock\u00e9s dans une base de donn\u00e9es SQLITE. On pourrait se dire Banco il suffit d'aller dedans et de retrouver tous les cookies et se connecter. Sauf que, pas b\u00eates, les \u00e9quipes de chrome ont d\u00e9cid\u00e9 que c'\u00e9tait une bonne id\u00e9e d'encoder les cookies pour que tout le monde ne puisse pas venir y mettre son nez... En effet les cookies peuvent contenir des informations importantes. Cela fait que pour utiliser ces cookies il faut pouvoir les d\u00e9coder. Mon hypoth\u00e8se a \u00e9t\u00e9 que si ces cookies peuvent \u00eatre lus par Chrome m\u00eame hors connexion, c'est que la cl\u00e9 de d\u00e9codage existe sur l'appareil et qu'il suffit de la trouver. ET C'EST LE CAS! Apr\u00e8s pas mal de recherches j'ai pu voir que la cl\u00e9 de d\u00e9codage existe bel et bien et qu'il suffit de la d\u00e9coder en utilisant la librairie DPAPI pour la lire. Avec cette cl\u00e9 on peut ensuite d\u00e9coder les cookies et leurs valeurs ce qui veut dire qu'il est th\u00e9oriquement possible d'automatiser le processus sans que l'utilisateur n'aie rien \u00e0 faire. J'ai d\u00e9cid\u00e9 de faire la partie r\u00e9cup\u00e8ration en python pour deux raison : Je n'arrivais pas \u00e0 trouver une bonne impl\u00e9mentation de DPAPI en C# qui me permettait de d\u00e9coder la cl\u00e9. Il existe beaucoup plus de documentation en Python pour ce qui est de la cryptographie et donc si Chrome change de fonctionnement il sera beaucoup plus simple de changer cette partie en particulier sans avoir \u00e0 recompiler le code C#. J'ai donc avec l'aide d'internet et de ChatGPT cr\u00e9\u00e9 ce script : def get_master_key(): with open( os.getenv(\"localappdata\") + \"\\\\Google\\\\Chrome\\\\User Data\\\\Local State\", \"r\" ) as f: local_state = f.read() local_state = json.loads(local_state) master_key = base64.b64decode(local_state[\"os_crypt\"][\"encrypted_key\"]) master_key = master_key[5:] # removing DPAPI master_key = win32crypt.CryptUnprotectData(master_key, None, None, None, 0)[1] print(\"MASTER KEY :\") print(master_key) print(len(master_key)) return master_key def decrypt_payload(cipher, payload): return cipher.decrypt(payload) def generate_cipher(aes_key, iv): return AES.new(aes_key, AES.MODE_GCM, iv) def decrypt_password(buff, master_key): try: iv = buff[3:15] payload = buff[15:] cipher = generate_cipher(master_key, iv) decrypted_pass = decrypt_payload(cipher, payload) decrypted_pass = decrypted_pass[:-16].decode() # remove suffix bytes return decrypted_pass except Exception: # print(\"Probably saved password from Chrome version older than v80\\n\") # print(str(e)) return \"Chrome < 80\" master_key = get_master_key() cookies_path = Path( os.getenv(\"localappdata\") + \"\\\\Google\\\\Chrome\\\\User Data\\\\Default\\\\Network\\\\Cookies\" ) if not cookies_path.exists(): raise ValueError(\"Cookies file not found\") with sqlite3.connect(cookies_path) as connection: connection.row_factory = sqlite3.Row cursor = connection.cursor() cursor.execute(\"SELECT * FROM cookies\") with open('cookies.csv', 'a', newline='') as csvfile: fieldnames = ['host_key', 'name', 'value', 'path', 'expires_utc', 'is_secure', 'is_httponly'] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) if csvfile.tell() == 0: writer.writeheader() for row in cursor.fetchall(): decrypted_value = decrypt_password(row[\"encrypted_value\"], master_key) writer.writerow({ 'host_key': row[\"host_key\"], 'name': row[\"name\"], 'value': decrypted_value, 'path': row[\"path\"], 'expires_utc': row[\"expires_utc\"], 'is_secure': row[\"is_secure\"], 'is_httponly': row[\"is_httponly\"] }) print(\"Finished CSV\") Ce programme va faire tout ce que j'ai expliqu\u00e9 et va ensuite stocker les r\u00e9sultats dans un CSV pour qu'il soit facile d'y acc\u00e8der depuis le C#. Alors oui cela pose certaines questions de s\u00e9curit\u00e9. Car en effet je prend tous les cookies, les d\u00e9code et les stocke. Ce qui veut dire que je pourrais tout \u00e0 fait envoyer ces donn\u00e9es quelque part, par exemple un compte Netflix, et me rincer. Si je devais rendre le projet ouvert au public je pense qu'il faudra que cela soit mentionn\u00e9 clairement et que le projet soit open source pour que les utilisateurs puissent verifier que je ne fais pas ca. Maintenant de l'autre c\u00f4t\u00e9 j'ai juste \u00e0 lire le CSV et le tour est jou\u00e9 ! (Trouver cette solution m'a pris une semaine de vacances \u00e0 l'\u00e9poque) Bon j'ai r\u00e9ussi \u00e0 faire le programme se connecter et naviguer etc.. Par contre quelque chose que j'ai voulu ajouter et qui m'a pris pas mal de temps c'est de faire en sorte de pouvoir selectionner la qualit\u00e9. Pour changer la qualit\u00e9 du feed il faut cliquer sur settings et ensuite prendre le menu deroulant et selectioner 1080p. Le soucis c'est le que la value du select est jamais la m\u00eame. Elle commence toujours pas \"1080_\" mais ensuite ca peut \u00eatre \"1080_45930285\" ou \"1080_56801\" la suite est apparemment random. J'ai donc du utiliser ce code pour le selectioner quand m\u00eame : IWebElement settingsButton = driver.FindElement(By.ClassName(\"bmpui-ui-settingstogglebutton\")); settingsButton.Click(); IWebElement selectElement = driver.FindElement(By.ClassName(\"bmpui-ui-videoqualityselectbox\")); SelectElement select = new SelectElement(selectElement); IWebElement selectOption = selectElement.FindElement(By.CssSelector(\"option[value^='1080_']\")); selectOption.Click(); Sauf que pour que cela marche je dois avant cliquer sur le bouton des settings le probl\u00e8me c'est qu'il est invisible alors on doit le faire apparaitre. J'ai tent\u00e9 de le faire aparaitre en bougeant la souris, en cliquant \u00e0 un endroit pr\u00e9cis, impossible de le faire marcher correctement. Puis j'ai eu l'id\u00e9e de mettre pause en envoyant un appui sur la touche Espace et ca a permit de d\u00e9couvrir le bouton et permettre qu'on clique dessus. Ca peut paraitre tout b\u00eate mais rien que ca, ca m'a pris un temps consid\u00e9rable. Bon pour ce qui est du timecode de la vid\u00e9o. Je pense qu'il serait trop complexe de faire en sorte que selenium change le slider de progression de la vid\u00e9o. Alors j'ai fait quelques tests et apparemment, si on quitte la F1TV sur un timecode de la vid\u00e9o que on donne au programme, comme il r\u00e9cup\u00e8re tous les cookies de la F1TV il commencera de la. Donc si on veut utiliser le programme avec des Grand Prix ayant d\u00e9ja eu lieu, on peut le faire, seulement il faudra juste au pr\u00e9alable avoir choisit le bon timecode dans le page de la F1TV avant de le lancer. Ce qui est int\u00e9ressant c'est que la page de la F1TV ressemble \u00e0 ca au d\u00e9part : \"Empty F1TV\" Je pense qu'une bonne id\u00e9e serait de dire au programme que c'est la grille de d\u00e9part et ensuite d\u00e8s qu'il d\u00e9tecte un secteur il sait que la course a commenc\u00e9. Lundi 24 Avril 2023 Aujourd'hui c'est jour de documentation. J'ai pas mal travaill\u00e9 pendant les vacances mais je n'ai pas encore pu faire de vraie documentation correcte du fonctionnement. Du coup je vais m'en charger aujourd'hui et peut-\u00eatre un peu demain. Ok normalement je ne devrais faire que de la documentation mais je ne peux pas passer \u00e0 cot\u00e9 de ca... Le probl\u00e8me que j'ai avec les pneus ou parfois il d\u00e9tecte un H au lieu d'un '11' et ce genre de choses c'est \u00e0 cause de ma methode \"RemoveBG\" Qui va retirer tous les pixels plus sombres que le background. Sauf que cela va aussi retirer des pixels dans le chiffre lui m\u00eame et qui va donc defigurer les 11 : \"diformed 11\" \"diformed 11\" J'ai r\u00e9ussi \u00e0 les changer en : \"less diformed\" \"less diformed\" Mais au final cela n'a pas augment\u00e9 la pr\u00e9cision de la reconnaissance. Je pense que je vais donc devoir encore changer. Je pense que une bonne facon de trouver serait dabord de trouver la couleur du pneu. Et si il n'y a pas assez de couleur alors c'est que le pneu contient une lettre. Le but est d'arr\u00eater de chercher des lettres ou des chiffres. Comme ca les 11 arr\u00eateront d'\u00eatre pris pour des 'H' En fait on peut faire encore plus simple que ca. On peut simplement regarder la couleur dominante et determiner le pneu. En effet m\u00eame si il y a une lettre sur fond noir pour d\u00e9crire le pneu, mon methode de r\u00e9cup\u00e8ration de la couleur dominante ommet les pixels trop noirs alors il est quand m\u00eame possible de determiner le type de pneus. Et tout simplement si il n'arrive pas \u00e0 lire le chiffre c'est que c'est une lettre et que donc on est \u00e0 0 tours. Cela marche plut\u00f4t bien et cela simplifie pas mal le processing. Voila, la je vais me remettre \u00e0 la documentation sinon je vais encore prendre du retard. Mardi 25 Avril 2023 Encore une fois j'ai pris du temps de doc pour changer des choses sur la partie OCR. Mais en m\u00eame temps en documentant je vois des choses que j'ai soit mal fait soit que je pourrais faire mieux en changeant tr\u00e8s peu de choses. J'\u00e9sp\u00e8re que les changement que j'ai fait vont aider au moins \u00e0 la coh\u00e9rence du code et un peu pour les performances. Il semble que dans les conditions que j'ai test\u00e9 le nombre de tour soit plut\u00f4t fiable mais je pense que je devrai faire un peu de travail en aval dans la r\u00e9cup\u00e9ration de ces donn\u00e9es car je sens que cela va poser probl\u00e8me quelques fois. Je pense que en utilisant bien l'historique on peut potentiellement se passer de l'utilisation de ce chiffre pas toujours compl\u00eatement fiable. Mais sinon aujourd'hui c'est encore une fois un gros jour de doc. J'essaie d'expliquer les diff\u00e9rents proc\u00e9d\u00e9s avant de les oublier. J'essaie aussi de donner un maximum d'exemples sous formes de photos interm\u00e9diaires mais ca me prend pas mal de temps car il faut que j'ajoute un peu partout dans le code des lignes pour sortir des images interm\u00e9diaires. En plus de la documentation je me suis aussi beaucoup occup\u00e9 de nettoyer mon code et je suis assez content par ce que m\u00eame en ayant du rajouter des couches de complexit\u00e9 pour mieux reconnaitres les temps au tour j'arrive \u00e0 un temps de processing parfois en dessous des 2 secondes ce que je trouve honorable. Quand j'aurai finit de nettoyer tous mes fichiers je ferai une release sur gitea et ce sera la version que j'utiliserai quand je voudrai faire un merge avec les autres parties du projet. J'ai beaucoup beaucoup boss\u00e9 aujourd'hui et je sui bien mort. Faire autant de documentation et de nettoyage de code c'est pas forc\u00e9ment bon pour le cerveau je crois. J'ai besoin d'une sieste. Demain je pense que je vais commencer \u00e0 avancer sur la partie r\u00e9cup\u00e8ration des images. Je sais que la je fais un peu passer les tests \u00e0 la trappe mais d\u00e9ja j'en ai fait tout le long du d\u00e9veloppement de OCR_DECODE et il faut vraiment que j'avance, quitte \u00e0 revenir dessus quand j'aurai merge les deux projets ensemble. 26 Avril 2023 Aujourd'hui je vais devoir m'occuper de la partie r\u00e9cup\u00e9ration des images. J'ai d\u00e9ja eu l'occasion d'avancer sur ce projet pendant mopn poc et mes vacances. Donc la le but ca va \u00eatre de voir ce qui manque comme v\u00e9ritables features et ensuite je vais pouvoir m'occuper de la vue et de son int\u00e9gration avec le d\u00e9codage. Ok donc maintenant que j'au un programme qui arrive \u00e0 prendre des images depuis la F1TV correctement et en bonne r\u00e9solution. Je pense qu'il est temps de passer \u00e0 l'impl\u00e9mentation de la Forme que ca va prendre. C'est important de se poser au moins cinq minutes la question de comment je pr\u00e9vois de faire car m\u00eame si ca n'est pas la version finale, cette derni\u00e8re prendra tr\u00e8s fort inspiration du desing que je vais faire. Dans cette form j'aurais besoin de : Pouvoir selectionner un Grand Prix en ins\u00e8rant l'URL du feed. Pouvoir lancer la calibration si besoin Indiquer le titre et la date du Grand Prix Indiquer si le Grand Prix vient de commencer ou si il y a d\u00e9ja un certain nombre de tours lanc\u00e9s. Et c'est \u00e0 peu pr\u00e8s tout en fait... J'ai tellement pouss\u00e9 pour avoir un programme qui fait tout tout seul que il ne me faut pas grand chose de plus. Je pense que ce qui serait pas mal ca serait du coup d'utiliser ce temps pour bien impl\u00e9menter la calibration qui elle aura besoin d'une UI un peu plus bal\u00e8ze. On pourrait m\u00eame imaginer que la calibration fasse partie int\u00e9grante des settings... Ca serait peut-\u00eatre bien que quand l'application se lance on se retrouve sur la page principale d'affichage de donn\u00e9es et qu'on puisse simplement cliquer sur la page options qui contient la page calibration et qui permet de rentrer les infos du Grand Prix. Je pense que je vais faire ca. Voici l'interface que j'ai d\u00e9velopp\u00e9e pour regrouper tout ca : \"Screen\" La police le style le placement et les couleurs ne sont pas d\u00e9finitfs, cependant je pense que c'est un bon d\u00e9but. Le but maintenant va \u00eatre de permettre de faire fonctionner la calibration et la r\u00e9cup\u00e8ration d'images. Si j'arrive \u00e0 faire fonctionner ces deux choses sur un m\u00eame projet avant la fin de la semaine cela serait super ! Bon J'ai pu avancer sur l'int\u00e8gration de Selenium mais cela prend un peu de temps car je veux impl\u00e9metner un moyen de pouvoir prendre une Screenshot \u00e0 nimporte quel moment et pas juste en boucle. Demain je finis de faire fonctionner ca et ensuite je commence le cablage du reste. Jeudi 27 Avril 2023 C'est assez dur de faire l'importation car il y a des petites diff\u00e9rences qui obligent \u00e0 presque tout r\u00e9\u00e9crire. En fait le programme de calibration avait d\u00e9ja impl\u00e9ment\u00e9 la fonction de Windows et de Zones mais il fonctionnait juste assez diff\u00e9remment pour qu'il faille tout refaire. La je suis en train de perdre \u00e9norm\u00e9ment de temps \u00e0 cause d'un soucis de coordonn\u00e9es. J'ai repris le code de la calibration pour detecter ou l'utilisateur a cliqu\u00e9 pour cr\u00e9er les zones. Cependant, je n'arrive pas \u00e0 le faire fonctionner correctement. La zone est tout le temps d\u00e9cal\u00e9e en haut et en bas mais pas de la m\u00eame facon. En haut, la valeur Y est trop grande alors que en bas la valeur Y est trop petite... Je ne comprends pas bien pourquoi. Si c'\u00e9tait un simple d\u00e9calage cela ne serait pas compliqu\u00e9 \u00e0 g\u00e8rer mais la... J'ai un soucis \u00e9galement avec la r\u00e9solution des screenshots que je r\u00e9cup\u00e8re en full Headless. Voici un exemple de r\u00e9solution que j'arrive \u00e0 r\u00e9cup\u00e8rer sans le headless : \"High Res\" \"Low Res\" Il y a clairement un soucis et le probl\u00e8me c'est que avec une r\u00e9solution pareille, impossible de faire une reconnaissance correcte. BON J?EN PEUX PLUS LA. Ca fait des heures que je bosse sur ce probl\u00e8me d\u00e9bile et impossible de trouver une solution. J'ai essay\u00e9 cinq facons de forcer le browser headless a prendre une plus haute r\u00e9solution aucune ne fonctionne je ne comprends pas. A chaque fois que je me retrouve avec une r\u00e9solution de 1366 x 768 Ou une variante de basse r\u00e9solution du style. J'en peux plus je ne trouve aucune r\u00e9ponse sur internet ni m\u00eame avec chatGPT. Super... La seule chose que j'ai pu faire qui change quelque chose fait que les images font maintenant du 926x517... j'ai un peu envie de commentre un crime de guerre au plus vite. Vendredi 28 Avril 2023 Une des solutions que je n'ai pas encore essay\u00e9 est de changer ma version de GeckoDriver. Sauf que ca m'oblige \u00e0 changer les versions de mes libraries ce qui est tr\u00e8s p\u00e9nible, je vais continuer le debugging dans le projet Selenium_clean. Il faut savoir que la librairie de Selenium que j'utilise est bloqu\u00e9e en 0.27 ce qui fait que je ne peux utiliser qu'une version obsol\u00e8te du Gecko Driver. J'ai tent\u00e9 de changer vers une version en 64 bits du GeckoDriver 0.27 mais pareil, je me retrouve toujours avec des images de M. J'essaie toutes les solutions que je trouve sur internet aucune ne convient c'est infernal. J'essaie de changer la r\u00e9solution DPI, j'essaie de changer les param\u00eatres par d\u00e9faut des player de Firefox, j'essaie de changer la r\u00e9solution pendant et au d\u00e9but de l'execution IMPOSSIBLE DE FAIRE MARCHER CETTE MERDE C'EST PAS POSSIBLE !!! J'ai essay\u00e9 avec chrome mais je ne peux pas l'utiliser car les DRM m'emp\u00eacheront de prendre des screenshot du flux vid\u00e9o. J'ai essay\u00e9 de faire tourner avec edge mais edge ne peut pas tourner en headless. JE VAIS DEVENIR FOUF FPWQOVMQEKOVNVIBDBJDAIVOBI. ET MAINTENANT JE N'ARRIVE PLUS A FAIRE DE PROJET AVEC SELENIUM VOIWQNV(UEWQBVU)WEQN=OEJNIVIUWVBWUEV ON CHERCHE A ME FAIRE PETER UN PLOMB C'EST PAS POSSIBLE GIWEGUWEQN VOICI UN EXEMPLE DU CODE QUE JE DEMANDE A UN NOUVEAU PROJET AVEC EXACTEMENT LES MEMES LIBRARIES INSTALLEES : // Create a new FirefoxDriver instance IWebDriver driver = new FirefoxDriver(); // Navigate to the specified URL driver.Navigate().GoToUrl(\"https://www.example.com\"); // Do something with the driver (e.g., find elements or take screenshots) // Quit the driver driver.Quit(); Je ne demande que ca ET MEME CA CA NE VEUT PAS FONCTIONNER VOIWENB)IWUQENV Oui je suis un peu \u00e9nerv\u00e9 ca se voit? A bon? Et maintenant NUGGET ne fonctionne plus... j'en peux plus la. Je ne peux plus t\u00e9l\u00e9charger de librairie sur aucun de mes projets... J'ai tent\u00e9 de supprimer le fichier de config et red\u00e9marrer Visual Studio mais cela ne fait rien. J'ai aussi tent\u00e9 de faire un 'nugget restore' toujours rien. Bon apparemment je ne suis pas le seul qui ne peut pas acc\u00e8der \u00e0 Nuget donc bon c'est pas juste chez moi qu'il y a un soucis. Mais m\u00eame en mettant ma 4G pour me connecter, je n'arrive pas \u00e0 acc\u00e8der \u00e0 certains sites y compris Nuget et je ne peux pas download de librairies... Je ne comprends pas ce qui se passe et du coup je ne peux juste pas bosser... J'ai red\u00e9marr\u00e9 trois fois mon pc et visual studio, j'ai essay\u00e9 de changer mes settings DNS etc... impossible de bosser. Je crois que je n'aurais pas du me reveiller aujourd'hui. Bon je vais tenter d'avancer sur mon poster en attendant que le r\u00e9seau soit en meilleur \u00e9tat. Lundi 1 Mai 2023 Bon je bosse depuis chez moi donc j'esp\u00e8re que Nuget va mieux fonctionner. Apr\u00e8s un weekend \u00e0 r\u00e9fl\u00e9chir au sujet de cette resolution je me suis dit deux choses. La seule personne sur internet que j'ai vu avoir le meme soucis avait une r\u00e9solution de 1920x1200 comme moi. Cela veut donc s\u00fbrement dire que le soucis vient de cette r\u00e9solution de laptop comme moi. Si vraiment je n'arrive pas dans un premier temps \u00e0 faire fonctionner le Headless correctement, je peux toujours laisser la page de c\u00f4t\u00e9 et m'occuper du reste du programme. Certes ca serait vraiment infernal d'avoir \u00e0 garder une page chrome ouvert en tous temps et en plus elle doit \u00eatre en plein \u00e9cran mais bon... Si il n'y a vraiment pas d'autres solutions malheureusement je serai bien oblig\u00e9. BON ! JE N'ARRIVE MEME PLUS A FAIRE UN PROJET QUI UTILISE SELENIUM ET QUI MARCHE JE VAIS FAIRE BR\u00dbLER GENEVE. C'est pas possible serieux, je ne comprends pas j'essaie tout ce que je trouve et impossible de juste lancer firefox c'est du grand nimporte quoi. Je prend les m\u00eame putain de librairies que sur les autres projets les m\u00eames versions, je prend le m\u00eame exact code. Sur le nouveau projet impossible de le faire fonctionner. Je commence \u00e0 croire que on essaie de me faire p\u00eater un cable. Du coup dans un \u00e9lan de d\u00e9sespoir je vais tenter de passer sur une autre librairie qui avec un peu de chance marche et en plus me permettrais de prendre des foutues screenshot dans le bon format. Les deux seules librairies qui pourraient potentiellement faire l'affaire sont les librairies : PhantomJS CefSharp Je vais les tester et simplement prier pour qu'elles fonctionnent et que je puisse faire ce que je veux avec. Alors pour le moment avec CEFSharp j'arrive \u00e0 lancer une instance de chrome et prendre une screenshot avec ce code : CefSettings settings = new CefSettings(); settings.CachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), \"CefSharp\\\\Cache\"); // Set cache path settings.LogSeverity = LogSeverity.Disable; // Disable logging Cef.Initialize(settings); // Initialize CEF using (var browser = new ChromiumWebBrowser(\"www.google.com\", new BrowserSettings())) // Launch Chromium in off-screen mode { browser.Load(\"https://www.example.com\"); // Navigate to the test URL browser.Size = new Size(1920, 1080); // Set the browser size to 1920x1080 browser.ScreenshotAsync().ContinueWith(task => { var bitmap = task.Result; bitmap.Save(\"screenshot.png\", System.Drawing.Imaging.ImageFormat.Png); // Take a screenshot and save it as a PNG file }).Wait(); } Cef.Shutdown(); // Shutdown CEF Avec ca il faut ces using : using System; using System.Drawing; using System.IO; using CefSharp; using CefSharp.OffScreen; C'est assez prometteur m\u00eame si il faut encore beaucoup pour remplacer selenium. Ah bah lol en fait non on peut pas utiliser cette librarie pour faire tourner firefox... J'EN AI MARRE J'AVAIS CHERCHE PRECISEMENT UNE LIB QUI MARCHE AVEC FIREFOX Et phantomJS non plus ne fonctionne pas avec firefox... J'en ai marre. Donc je vais plut\u00f4t partir sur la librairie GeckoFX qui semble pouvoir contr\u00f4ler une instance de firefox. Mais j'avais justement pris un putain de projet C# et pas JS pour ne pas me taper ces probl\u00e8mes de librairies... Et si cette option ne fonctionne pas mon dernier espoir sera de directement int\u00e9ragir avec le geckodriver.exe et la ca risque de pas \u00eatre dr\u00f4le. JE NE COMPRENDS RIEN !!!!! Ca n'a aucun sens la doc est inexistante le seul lien qui pourrait amener sur une doc envoie sur la page principale de bitbucket. Tous les exemples de code que je trouve ne fonctionnent pas. Je n'arrive \u00e0 rien je commence \u00e0 devenir fou. Tout ce travail pour rien c'est pas possible. M\u00eame en essayant directement d'int\u00e9ragir avec le process geckodriver.exe je ne peux pas arriver \u00e0 mes fins. J'arrive \u00e0 lancer le service et tout, mais je n'arrive pas \u00e0 vraiment contr\u00f4ler ce qu'il se passe donc impossible de venir prendre des screenshot. Je ne sais tout simplement pas quoi faire ... Je suis bloqu\u00e9. Je me suis cass\u00e9 la t\u00eate \u00e0 faire un truc qui marchait bien avec selenium et tout. Mais maintenant plus rien ne fonctionne du jour au lendemain et il n'y a simplement aucune alternative. Je vais essayer de changer directement le projet Selenium_Clean mais bon ca va pas \u00eatre dr\u00f4le. Ok alors j'ai tout repris depuis le d\u00e9but et je crois que j'ai enfin une solution. Pour la trouver j'ai re-essay\u00e9 toutes les techniques que j'avais tent\u00e9 avant mais dans l'ordre et en les isolant \u00e0 chaque fois. Cela inclus : Tenter de changer la densit\u00e9 de pixels. En effet je me suis dit que comme la r\u00e9solution \u00e9tait plus basse le soucis \u00e9tait que le virtual screen avait simplement une DPI r\u00e9duite. profile.SetPreference(\"layout.css.devPixelsPerPx\", \"2.0\"); J'ai aussi tent\u00e9 de r\u00e9duire \u00e0 un seule le nombre de process de Firefox. J'ai pu lire sur internet que parfois cela pouvait influer sur les performances du renderer. profile.SetPreference(\"dom.ipc.processCount\", 1); Ensuite j'ai tent\u00e9 tout b\u00eatement de rajouter dans la liste des arguments la taille voulue de l'\u00e9cran. options.AddArgument(\"--window-size=1920,1080\"); Mais comme cela ne foncionnait pas, je me suis rabattu sur un script JS pour tenter de forcer la fen\u00eatre \u00e0 \u00eatre plus grande. js.ExecuteScript(\"window.resizeTo(1920, 1080);\"); Comme cela n'a pas march\u00e9 j'ai pu lire que cela pouvait \u00eatre la taille int\u00e9rieure qui devait \u00eatre chang\u00e9e js.ExecuteScript(\"window.innerWidth = 1920; window.innerHeight = 1080;\"); Encore une fois sans succ\u00e8s. J'ai ensuite tent\u00e9 d'utiliser trois autres versions du GeckoDriver, 0.27,0.26,0.25 et aucune ne m'aidait. Mais en fait la seule chose qui a chang\u00e9 quoi que ce soit \u00e9tait la technique suivante : Changer la window size en utilisant : options.AddArgument(\"--width=1920\"); options.AddArgument(\"--height=1200\"); Ca ne marchait pas car j'utilisais une autre methode pour resize en m\u00eame temps, qui elle ne marchait pas mais qui emp\u00eachait celle la de marcher. Ensuite le soucis que j'avais c'est que en mettant 1920-1080 je me retrouvais avec 1920-998 ou un truc du genre ce qui n'\u00e9tait pas normal alors je me disais que cette technique ne marchait pas non plus et je l'ai pass\u00e9e. Alors tout n'est pas encore gagn\u00e9, il faut que j'arrive \u00e0 impl\u00e9menter ca dans un plus gros projet et que la vid\u00e9o puisse \u00eatre prise seule. Demain je m'occupe de ca. Mardi 2 Mai 2023 Bon aujourd'hui je change le programme principal. Le soucis que j'ai c'est que en ajoutant ce syst\u00e8me de resize, maintenant la page fait 100x100 et est grise. Il doit y avoir une technique que j'ai oubli\u00e9 de retirer ou un comportement un peu bizarrre. Bon clairement je ne sais pas QUI DECIDE DE ME POURRIR LA VIE mais il est fort. J'ai t\u00e9l\u00e9charger EXACTEMENT les m\u00eames librairies que sur mon autre projet et j'utilise l'EXACT m\u00eame geckodriver.exe mais dans le projet principal impossible de lui faire chier une image m\u00eame avec l'EXACT m\u00eame code. POURQUOI VOUS ME FAITES CA????= La je ne comprend vraiment pas ce qui peut se passer pour que rien ne fonctionne alors que tout est pareil. JE VIENS DE TOUT VERIFIER TOUT EST PAREIL JE NE COMPRENDS PAS. Bon apr\u00e8s avoir supprim\u00e9 l'int\u00e9gralit\u00e9 de ma classe Emulator cela semble marcher un peu mieux. Je ne vais pas m'\u00e9tendre sur la castrophe niveau temps que cela repr\u00e9sente. Si au moins j'arrive \u00e0 faire fonctionner quelque chose je suis content. Maintenant j'ai un soucis un peu sp\u00e9cial. Depuis que j'ai chang\u00e9 la r\u00e9solution, il semble que le programme aie du mal \u00e0 cliquer sur l'icone de settings. En prenant des screenshots du moment ou l'erreur apparait, j'ai pu me rendre compte que en fait le stream est toujours en train de charger et c'est pour ca que on arrive pas \u00e0 trouver le bouton : \"ERROR 105\" \"ERROR 105\" Je pense que je n'ai le soucis que maintenant car le flux en 1080p se lance moins vite. Je vais essayer de voir si je peux detecter un \u00e9l\u00e9ment d'HTML qui correspond au loading comme ca je peux attendre qu'il disparaisse. Sinon je peux aussi juste essayer de trouver le bouton en boucle pendant une dixaine de secondes. Bon la j'essaie pendant genre plus de 50 secondes et ca ne marche toujours pas. Il semblerait que au final le probl\u00e8me vienne du GP d'azerbidjan. En effet, quand je teste un autre Grand Prix tout va bien. ET MERDE ! J'ai r\u00e9ussi \u00e0 avoir des images en 1080P mais d\u00e9s que je passe l'image en plein \u00e9cran c'est de nouveau du 1366X768 Avant de mettre en plein \u00e9cran: \"Before fullscreen\" Apr\u00e8s: \"After fullscreen\" On peut voir sur l'image que l'option 1080P est effectivement bien selectionn\u00e9e mais il doit y avoir un param\u00e8tre de Firefox qui s'occupe de la r\u00e9solution d'un player vid\u00e9o. Il va juste falloir trouver ce param\u00eatre... J'ai essay\u00e9 d'utiliser : Driver.Manage().Window.Size = new System.Drawing.Size(windowWidth, windowHeight); Sans succ\u00e8s. options.AddArgument(\"--start-maximized\"); Pareil Driver.Manage().Window.Maximize(); Toujours rien profile.SetPreference(\"full-screen-api.ignore-widgets\", true); Nada profile.SetPreference(\"media.hardware-video-decoding.enabled\", true); Toujours pas J'ai vraiment cru que j'avais trouv\u00e9 la solution en trouvant cette commande profile.SetPreference(\"full-screen-api.enabled\", true); Mais non toujours pas... Je commence \u00e0 perdre patience. C'EST BON. Apr\u00e8s litt\u00e9rallement 3h de debugging avec M.Bonvin (Que je remercie IMMENSEMENT) on a r\u00e9ussi \u00e0 trouver au fin fond d'un thread github que la valeur \u00e9tait hard cod\u00e9e dans les variables d'environnement et que donc quoi que je fasse je n'aurais pas pu le changer. En fait la seul moyen de tout r\u00e8gler a \u00e9t\u00e9 de changer les variables d'environnement de ma machine: MOZ_HEADLESS_WIDTH et MOZ_HEADLESS_HEIGHT . Et ce qu'il y a de bien c'est que maintenant je peux mettre de la 4K et cela permet de faire un meilleur upscaling. Recrutement Payerne Mai 2023 J'ai du faire mon recrutement \u00e0 Payerne Mercredi et Jeudi. Si vous \u00eates curieux je peux vous dire que comme il n'y avait presque plus de places cet \u00e9t\u00e9 je ferai Canonnier Lance mines. C'\u00e9tait assez frustrant d'avoir perdu deux jours de travail mais on va faire avec. Vendredi 5 Mai 2023 Bon malgr\u00e9s les courbatures il faut que je me mette au boulot un peu serieusement par ce que sinon ca va \u00eatre compliqu\u00e9 de rattraper mon retard. La derni\u00e8re fois si je me souviens bien j'avais r\u00e9ussi \u00e0 trouver un moyen de prendres des images en bonne r\u00e9solution. Il faut maintenant que je commence \u00e0 faire fonctionner la calibration et ce qui serait bien ca serait que je commence \u00e0 ajouter la partie OCR au projet. Il faut que je me d\u00e9p\u00eache car Lundi je dois m'occuper du Poster. OK j'ai compris le soucis que j'avais quand j'essayais de faire la calibration. J'avais mis l'image en ZOOM ce qui fait que si la hauteur n'\u00e9tait pas la bonne, l'image \u00e9tait recentr\u00e9e ce qui fait que cela faussait totalement les r\u00e9sultats. Quand on fait en sorte que l'image prenne toute la place, les coordonn\u00e9es sont prises correctement. Voici un exemple d'ou en est la partie calibration. \"Exemple settings UI\" Normalement il me suffit d'impl\u00e9menter les windows, et on devrait relativement facilement ajouter les pilotes. Et voila. J'ai pu impl\u00e9menter les windows et les pilotes. Et je peux aussi exporter des presets et les loader. Bon le loading est un peu beugg\u00e9 au niveau de l'affichage mais il semble qu'il fonctionne bien quand je save les images. Lundi je m'occupe du poster etc.. mais je pense que la suite va \u00eatre l'impl\u00e9mentation de l'OCR. Lundi 8 Mai 2023 Aujourd'hui c'est journ\u00e9e Poster. Je pense que je ne vais pas finir la journ\u00e9e content car les limitations sont un peu trop pr\u00e9sentes. J'ai fait une version que Garcia pourrait accepter, c'est \u00e0 dire en noir et blanc et avec un tout petit peu plus de d\u00e9tail. \"Poster V3\" Le truc c'est que en blanc je trouve que ca ne marche pas super. Et le concept d'avoir trois parties au projet qui se posent autour d'un circuit c'est peut-\u00eatre pas la meilleure id\u00e9e. Je me suis dit que la bonne id\u00e9e serait peut-\u00eatre de prendre un autre circuit pour qu'il y aie bien trois parties : \"Poster V4\" Clairement ce poster doit faire partie des pires. C'est pas clair et ca part dans tous les sens. Je vais essayer avec un autre layout de circuit. \"Poster V5\" Je me suis ensuite dit que le circuit n'\u00e9tait peut \u00eatre tout simplement pas une bonne id\u00e9e. J'ai donc essay\u00e9 de faire quelque chose de plus classique avec juste un peu de background pour qu'on puisse \u00e9viter le soucis de la page blanche derri\u00e8re : \"Poster V6\" Puis je me suis dit que finalement le circuit me manquait. Alors j'ai d\u00e9cid\u00e9 de combiner le background et le circuit ainsi que simplifier l\u00e9g\u00e8rement les diagrammes en retouchant un peu tout le reste on pouvait arriver \u00e0 quelque chose de sympatique : \"Poster V7\" Je ne suis pas content \u00e0 100% mais bon je pense que je vais m'en satisfaire. Pour donner une id\u00e9e de la gal\u00e8re que c'est de cr\u00e9er un poster, voici ce \u00e0 quoi ressemble mon espace de travail Figma : \"Bordel Figma\" Je ne suis pas un graphiste et ca se voit '^^. Je pense que comme il me reste un peu de temps aujourd'hui, je vais faire un peu de documentation de la partie r\u00e9cup\u00e8ration d'images. En effet, je pense que je n'aurai plus besoin de changer grand chose \u00e0 ce niveau. Mais je ne ferai pas la partie analyse fonctionnelle car l'interface n'est clairement pas termin\u00e9e. En fait j'avais oubli\u00e9 mais j'ai eu un rendez vous m\u00e9dical du coup je n'ai pas eu trop le temps de faire la doc que je voulais. Mais au moins je pense avoir finit mon travail sur le poster et le abstract en Anglais qui sont les deux gros livrables \u00e0 venir. Mardi 9 Mai 2023 Bon je viens de me rendre compte que apparemment on doit rendre l'abstract anglais, le Poster, ET LE PROJET. Je pense que mes deux jours \u00e0 l'arm\u00e9e m'ont fait perdre un peu la notion du temps car j'avais l'impression que l'evaluation interm\u00e9diaire 1 \u00e9tait il y a genre moins d'une semaine. Donc aujourd'hui je ne vais pas trop avancer sur le code et vraiment me focus sur la documentation de la r\u00e9cup\u00e8ration d'images. Je pense que je vais aussi ajouter la partie calibration \u00e0 la documentation. Je pense que c'est important que je prenne le temps maintenant car sinon le prof aura l'impression que ca n'a pas trop avanc\u00e9 depuis la derni\u00e8re fois. Et puis je pense que la partie calibration et r\u00e9cup\u00e8ration d'images ne va pas trop changer et la partie calibration encore moins. La partie anglaise je fais la revoir un peu mais je l'avais d\u00e9ja faite pendant les premiers jours alors ca devrait aller. Pour le rendu il nous \u00e9tait demand\u00e9 de fournir un fichier PDF avec tout dedans avec une table des mati\u00e8res notre code source etc... Pour ce faire j'ai du changer le mkdocs.yml et installer des packages. Voici les changements :: site_name: Documentation Track Trends site_author: Rohmer Maxime copyright: \u00a9CFPTI Tech2 theme: name: material palette: # Palette toggle for light mode - media: \"(prefers-color-scheme: light)\" scheme: default toggle: icon: material/brightness-7 name: Switch to dark mode # Palette toggle for dark mode - media: \"(prefers-color-scheme: dark)\" scheme: slate toggle: icon: material/brightness-4 name: Switch to light mode markdown_extensions: - attr_list - md_in_html - pymdownx.highlight plugins: - glightbox - search - img2fig - with-pdf: cover_subtitle: Vroum Vroum enabled_if_env: ENABLE_PDF_EXPORT - annexes-integration: annexes: # Required (at least 1) - ConfigurationTool.cs: Code/ConfigurationTool.cs # An path to an annex with its title - DriverGapToLeaderWindow.cs: Code/DriverGapToLeaderWindow.cs # An path to an annex with its title - DriverPositionWindow.cs: Code/DriverPositionWindow.cs # An path to an annex with its title - F1TVEmulator.cs: Code/F1TVEmulator.cs # An path to an annex with its title - Program.cs: Code/Program.cs # An path to an annex with its title - Window.cs: Code/Window.cs # An path to an annex with its title - DriverData.cs: Code/DriverData.cs # An path to an annex with its title - DriverLapTimeWindow.cs: Code/DriverLapTimeWindow.cs # An path to an annex with its title - DriverSectorWindow.cs: Code/DriverSectorWindow.cs # An path to an annex with its title - Form1.cs: Code/Form1.cs # An path to an annex with its title - Reader.cs: Code/Reader.cs # An path to an annex with its title - Zone.cs: Code/Zone.cs # An path to an annex with its title - DriverDrsWindow.cs: Code/DriverDrsWindow.cs # An path to an annex with its title - DriverNameWindow.cs: Code/DriverNameWindow.cs # An path to an annex with its title - DriverTyresWindow.cs: Code/DriverTyresWindow.cs # An path to an annex with its title - OcrImage.cs: Code/OcrImage.cs # An path to an annex with its title - Settings.cs: Code/Settings.cs # An path to an annex with its title - recoverCookiesCSV.py: Code/recoverCookiesCSV.py # An path to an annex with its title Je remercie Monsieur Briard le sultan officiel de Mkdocs de la classe de m'avoir aid\u00e9 pour cette partie et avoir cr\u00e9\u00e9 un plugin qui me permet de mettre mon code source directement dans le pdf. Bon au final j'ai quand m\u00eame chang\u00e9 mon poster \"Poster V8\" Mais je suis trop attach\u00e9 \u00e0 l'ancien concept alors je vais plut\u00f4t utiliser ca : \"Poster V9\"","title":"Journal de bord"},{"location":"jdb.html#journal-de-bord","text":"","title":"Journal de bord"},{"location":"jdb.html#mercredi-29-mars-2023","text":"Premier jour du travail de dipl\u00f4me. Nous avons eu un briefing de mr Garcia et nous avons pu commencer \u00e0 pr\u00e9parer le travail. Nous avons eu les diff\u00e9rents fichiers nescessaires \u00e0 la bonne r\u00e9alisation du projet et je me suis mis \u00e0 faire les fichiers nescessaires La premi\u00e8re chose a \u00e9t\u00e9 de faire ce mkdocs dans lequel j'ai mis un fichier yml plut\u00f4t standart qui risque de changer au fur et \u00e0 mesure. Voici le premier yml : site_name: Documentation Diplome theme: name: material palette: # Palette toggle for light mode - media: \"(prefers-color-scheme: light)\" scheme: default toggle: icon: material/brightness-7 name: Switch to dark mode # Palette toggle for dark mode - media: \"(prefers-color-scheme: dark)\" scheme: slate toggle: icon: material/brightness-4 name: Switch to light mode markdown_extensions: - attr_list - md_in_html plugins: - glightbox - with-pdf Voici un example de \u00e0 quoi ca ressemble en forme de site \"Exemple mkdocs\" Ensuite il m'a fallu faire une version plus \u00e0 jour de mon cahier des charges car je n'y avait pas touch\u00e9 depuis novembre. J'ai envoy\u00e9 un mail \u00e0 mes enseignants pour qu'ils puissent y jeter un oeuil pour \u00eatre s\u00fbr que je n'ai rien chang\u00e9 qui les d\u00e9rangent. Monsieur Jayr m'a demad\u00e9 \u00e0 l'occasion de lui faire un planning type Gantt alors je me suis mis \u00e0 la t\u00e2che. J'ai fait un planning pr\u00e9visionnel et une l\u00e9gende les deux sont dispo dans le dossier planning de ce repertoire. Ensuite je me suis mis \u00e0 tout mettre sur git. A commencer par ce repertoire Et c'est deja la fin de la journ\u00e9e ! Demain j'avance un peu sur la doc avec ce que je peux d\u00e9ja remplir et je finis de pr\u00e9parer ce dont j'ai besoin pour commencer \u00e0 coder.","title":"Mercredi 29 Mars 2023"},{"location":"jdb.html#jeudi-30-mars-2023","text":"Aujourd'hui selon le planning je dois me charger des dernirers pr\u00e9paratifs pour commencer correctement. J'ai fait expr\u00e8s de prenre du temps pour ca au d\u00e9but pour ne pas me cr\u00e9er de soucis plus loin pendant le travail. Je vais envoyer par mail le planning que j'ai fait \u00e0 mes suiveurs. Ensuite je vais m'attaquer au squelette de la docmentation. Je vais essayer de remplir tout ce que je peux remplir dans un premier temps car cela tout ca de fait pour plus tard quitte \u00e0 modifier quelques aspects au fur et \u00e0 mesure. J'ai aussi d\u00e9sactiv\u00e9 mkdocs with pdf par ce que les r\u00e9sultats ne sont vraiment pas ceux que j'attends et cela ralentis \u00e9norm\u00e9ment le d\u00e9ploiment. J'ai aussi rassembl\u00e9 mes croquis pour le poster : \"Croquis Poster 1\" \"Croquis Poster 2\" On peut voir que dans un premier temps j'ai tent\u00e9 de faire un poster un peu plus stylis\u00e9 et marketing. Cependant apr\u00e8s avoir discut\u00e9 avec Mr Garcia et diff\u00e9rents profs dont un de l'HEPIA et ils m'ont indiqu\u00e9 que ce qui \u00e9tait attendu \u00e9tait moins du marketing qu'un diagramme de fonctionnement. On peut voir sur les derniers posters que le cot\u00e9 technique ressort de plus en plus. Le but sera de faire une version encore plus technique ou on peut voir les diff\u00e9rents fonctionnements de l'application avec les technologies utilis\u00e9es. Le d\u00e9fi cela va \u00eatre de faire un joli poster qui soit en m\u00eame temps vendeur et en m\u00eame temps rempli techniquement. Oh et j'ai eu un probl\u00e8me ou mon calvier et ma souris ne voulaient d'un coup plus fonctionner. Dans mon cas c'\u00e9tait un probl\u00e8me de power management des ports. J'ai eu le soucis sur mon pc fixe \u00e0 la maison et sur mon pc portable \u00e9galement. En gros de ce que j'ai compris le soucis c'est que le pc croit que un port est trop solicit\u00e9 niveau puissance et du coup d\u00e9cide de couper l'alimentation du port USB. J'ai pu r\u00e8gler le soucis en allant dans le device manager sous universal bus controller sous power management et en d\u00e9cochant la case qui indique que windows peut d\u00e9sactiver ce port. Je ne conseille pas ce fix si vous avez des composants de mauvaise qualit\u00e9 car cela pourrait \u00eatre une vraie alerte cependant le fait que mes composants sont plut\u00f4t haut de gamme et le fait que mon clavier et ma souris le fassent en m\u00eame temps et que ils fonctionnaient tr\u00e8s bien depuis plus de 4 ans me font penser que c'est juste une nouvelle mise a jour de windows qui est p\u00e9nible. Demain je vais pouvoir commencer \u00e0 coder pour de bon.","title":"Jeudi 30 Mars 2023"},{"location":"jdb.html#vendredi-31032023","text":"Aujourd'hui on s'occupe de la PT2 qui est la programmation de la r\u00e9cup\u00e8ration des informations des images. Je vais tester IronOcr Source : https://www.c-sharpcorner.com/article/ocr-using-tesseract-in-C-Sharp/ Doc : https://ironsoftware.com/csharp/ocr/docs/ Examples : https://ironsoftware.com/csharp/ocr/examples/simple-csharp-ocr-tesseract/ Avant d'utiliser la librairie je me demande si je dois utiliser un peu de post processing pour aider \u00e0 la reconnaissance. Je peux soit utiliser l'image crop\u00e9e directement : \"Image non trait\u00e9e\" Soit avec un filtre pour passer en noir et blanc laxiste \"Image trait\u00e9e laxiste\" Soit avec un filtre pour passer en noir et blanc stricte \"Image trait\u00e9e stricte\" Il va falloir faire des tests avec tous les noms et les chiffres pour trouver le plus efficace. Bon malheureusment Iron OCR semblait \u00eatre une bonne alternative mais c'est une librairie priv\u00e9e qui demande une license pour \u00eatre utilis\u00e9e. Il va falloir trouver autre chose. En utilisant la librairie \"Tesseract\" qui existe on peut faire de la reconnaissance de texte avec un code plut\u00f4t simple : TesseractEngine engine = new TesseractEngine(tessDataFolder.FullName,\"eng\", EngineMode.Default); var tessImage = Pix.LoadFromMemory(ImageToByte(sample)); Page page = engine.Process(tessImage); string text = page.GetText(); Voici la methode ImageToByte : https://stackoverflow.com/questions/7350679/convert-a-bitmap-into-a-byte-array public static byte[] ImageToByte(Image img) { using (var stream = new MemoryStream()) { img.Save(stream, System.Drawing.Imaging.ImageFormat.Png); return stream.ToArray(); } } Voici le code pour traiter plusieurs textes sur une seule image : Page page = engine.Process(tessImage); // Get the iterator for the page layout using (var iter = page.GetIterator()) { // Loop over the elements of the page layout iter.Begin(); do { // Declare a Rect variable to hold the bounding box Rect boundingBox; // Get the bounding box for the current element if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox)) { g.DrawRectangle(Pens.Red,new Rectangle(boundingBox.X1,boundingBox.Y1,boundingBox.Width,boundingBox.Height)); } // Get the text for the current element var text = iter.GetText(PageIteratorLevel.Word); tbxResult.Text += text.ToUpper() + Environment.NewLine; } while (iter.Next(PageIteratorLevel.Word)); } Etonnament, avec plus de texte, des noms qui \u00e9taient autrefois mal reconnus sont parfaitement interpr\u00eat\u00e9s. Par exemple voici un exemple de reconnaisance de texte sur tous les pilotes : \"Screenshot de reconnaisance d'image complete\" On voit que le nom Leclerc est mal reconnu. Mais voici ce que cela donne quand on prend une image qui ne contient que le nom Leclerc : \"Screenshot de reconnaissance d'image crop\u00e9e\" On voit ici que le nom Leclerc est tr\u00e8s bien reconnu. Dans le premier exemple on peut voir que Tsunoda est reconnu comme \"Reticin\" ce qui n'est pas exactement pareil (lol) Et quand on isole le nom Tsunoda dans une image seule : \"Screenshot de reconnaissance de Tsunoda\" Il le lit \"RETLELYY\" ce qui n'est toujours pas exactement ca... Une meilleure r\u00e9solution pourrait peut-\u00eatre r\u00e9soudre le probl\u00e8me en partie. Jusqu'ici les images \u00e9taient en presque 720P ce qui donne ceci : \"Tsunoda en 720P\" Et j'ai lanc\u00e9 une r\u00e9cup\u00e8ration d'images en 1080p pour r\u00e9cup\u00e8rer ceci : \"Tsunoda en 1080P\" On peut voir une certaine diff\u00e9rence tout de m\u00eame. Et quand on lance la reconnaissance : \"Reconnaissance Tsunoda en 1080P\" \"Tsunoda n'est plus \u00e9crit \"RETLELYY\" mais \"TSUNDDA\" ce qui n'est pas parfait mais qui est d\u00e9ja beaucoup mieux. J'ai essay\u00e9 de mettre l'engine de Tesseract en mode \"JPN\" comme Tsunoda est un nom japonais mais sans succ\u00e8s j'ai le m\u00eame r\u00e9sultat. Comme la r\u00e9solution est meilleure je me suis dit que peut \u00eatre le filtre de passage en noir et blanc pourrait aider. J'ai \u00e9crit cette petite methode pour convertir l'image en noir et blanc : private static Bitmap ConvertToBlackAndWhite(Bitmap inputBmp) { const int BLACK_TO_WHITE_TRESHOLD = 200; Bitmap result = new Bitmap(inputBmp.Width, inputBmp.Height); for (int y = 0; y < inputBmp.Height; y++) { for (int x = 0; x < inputBmp.Width; x++) { Color pixelColor = inputBmp.GetPixel(x,y); if (pixelColor.R <= BLACK_TO_WHITE_TRESHOLD && pixelColor.G <= BLACK_TO_WHITE_TRESHOLD && pixelColor.B <= BLACK_TO_WHITE_TRESHOLD) { pixelColor = Color.FromArgb(0,0,0); } else { pixelColor = Color.FromArgb(255,255,255); } result.SetPixel(x,y,pixelColor); } } return result; } Rien de bien dingue mais cela fonctionne et je peux jouer avec le BLACK_AND_WHITE_TRESHOLD pour changer son comportement. J'ai dabord test\u00e9 avec un treshold de 100 et le programme a r\u00e9ussi \u00e0 me sortir Tsunoda en deux mots ce qui \u00e9tait d\u00e9ja tr\u00e8s encourageant. Et apr\u00e8s avoir augment\u00e9 le Treshold... Tada : \"Tsunoda 1080P avec filtre\" Le programme arrive bien \u00e0 reconnaitre TSUNODA. Je pense que cette tactique ne fonctionnait pas avant car la resolution \u00e9tait trop faible et l'aliasing se m\u00ealait trop avec le texte pour \u00eatre utilisable. Cependant cette technique ne fonctionne pas sur tous les noms. Par example avec Leclerc : \"Leclerc 1080P avec filtre\" On r\u00e9cup\u00e8re \"Leeler'c\" ce qui n'est pas bon du tout. Mais en modulant le Treshold (ici \u00e0 150) On peut de nouveau voir Leclerc \u00eatre reconnu correctement \"Leclerc 1080P avec filtre 2\" Je pense que pour avoir de bons r\u00e9sultats il va falloir faire un algo qui : D\u00e9coupe l'image en autant de plus petites images pour avoir un mot par image. Teste voir si avec l'image originale un nom correspond \u00e0 la liste de pilotes existant. Si cela ne marche pas, on applique le filtre en modulant le Treshold. Dans le cas ou on aurait pas un match parfait on fait un algo qui cherche le nom le plus proche qui existe dans la liste de noms donn\u00e9s. Seulement voila, il n'y a pas que des lettres que l'on veut r\u00e9cup\u00e8rer. On veut surtout pouvoir r\u00e9cup\u00e8rer les chiffres. Pour les chiffres on va avoir des soucis \u00e9galement... Si on essaie directement la m\u00eame technique sans filtre on a des r\u00e9sultats comme celui ci : \"Tentative de reconnaisance du timing\" La virgule a tendeance \u00e0 se barrer ce qui est particuli\u00e8rement probl\u00e9matique. Cependant comme les chiffres ont beaucoup moins de possibilit\u00e9es que les lettres et qu'il n'y a pas de probl\u00e8me de langue on devrait pouvoir travailler \u00e0 faire des r\u00e8glage que l'on pourra ensuite utiliser. Avec un Treshold de 165 on arrive presque \u00e0 quelque chose d'int\u00e9ressant : \"Tentative 2 de reconnaissance du timing\" Le + n'est clairement pas compris mais ca n'est pas emb\u00eatant car c'est souvent redondant. On arrive cependant \u00e0 isoler 3 et 259. M\u00eame si la virgule n'est pas comprise cela veut dire qu'il est tout de m\u00eame possible de discriminer les secondes des milisecondes. Maintenant avec un temps au tour : \"Reconnaissance du timing au tour\" On arrive sans rien changer aux param\u00eatres \u00e0 isoler minutes secondes et milisecondes. Il semble que la reconnaissance de chiffre soit bien plus efficace que la reconnaissance de lettres. Il va falloir faire un test \u00e0 plus grande \u00e9chelle avec plus d'image pour se rendre compte de la precision. Demain ce qui serait bien cela serait que je fasse un jeu d'images avec des valeurs connues et que je fasse une batterie de tests pour voir \u00e0 quel point je peux faire confiance \u00e0 la reconnaissance des chiffres. Automatiser un syst\u00e8me de test de la sorte me sera tr\u00e8s utile dans le futur pour v\u00e9rifier la non regression de ma reconnaissance de texte quand je tenterai d'y faire des changements. Je suis toujours curieux cependant de voir comment le programme se d\u00e9brouille avec les nombres de tours qui se trouvent dans les icones de pneus.","title":"Vendredi 31/03/2023"},{"location":"jdb.html#lundi-3-avril","text":"Aujourd'hui on va faire un programme qui permet de cr\u00e9er un dataset qui permette de tester le programme de reconnaissance. Je pense que le meilleur moyen de faire serait un programme qui cr\u00e9e le dataset et qui ensuite peut tester diff\u00e9rentes methodes de reconnaissance. Par la m\u00eame occasion je peux d\u00e9velopper la technologie qui va permettre de d\u00e9couper une image en 20 lignes ce qui me servira ensuite pour la reconnaissance. Je me rend compte que pour faire un programme de tests je dois d\u00e9ja avoir une id\u00e9e de la structure de mon programme. Pour le moment je r\u00e9flechis \u00e0 un syst\u00e8me de \"Zones\" et de \"Windows\". L'id\u00e9e serait que une Zone est juste une sous partie d'image qui peut encore \u00eatre d\u00e9compos\u00e9 tandis que chaque Window contient une ou plusieurs informations \u00e0 r\u00e9cup\u00e8rer. J'ai essay\u00e9 de d\u00e9couper l'image pour que cela soit plus clair : \"Main zone\" Ici on peut voir que l'image est d\u00e9coup\u00e9e en plusieurs grandes zones. Dans un premier temps on ne s'occupe que de la premi\u00e8re. Ensuite : \"Driver zone #1\" On peut voir la que cette Main zone serait elle m\u00eame d\u00e9compos\u00e9e en plusieurs plus petites zones. Et ensuite chacunes de ces petites zones : \"Driver windows\" Sera d\u00e9compos\u00e9e en plusieurs windows qui elles sont des zones qui contiennent de l'information. En gros on aurait trois types de zone : Les zones qui contiennent d'autres zones Les zones qui contiennent des Windows Les Windows Cependant en y r\u00e9flechissant on pourrait tout \u00e0 fait avoir seulement des zones et des windows en faisant en sorte que les windows peuvent avoir une liste de windows et une liste de zones. Une zone serait compos\u00e9e de : Une image de d\u00e9part Un rectangle qui la positionne sur cette derni\u00e8re Une liste de zones (potentiellement vide) Une liste de windows (potentiellement vide) Une methode qui permet de r\u00e9cup\u00e8rer une image de la zone Une methode qui permet de lancer la reconnaissance sur chaque window Une window serait compos\u00e9e de : Une image de d\u00e9part (cela peut \u00eatre l'image crop\u00e9e de la zone parente peu importe) Un rectangle qui la positionne sur cette derni\u00e8re Une methode qui permet de r\u00e9cup\u00e9rer un image de la window Une methode qui permet de lancer la reconnaisance sur l'image (Chaque type de zone doit l'impl\u00e9menter) Dans chaque window on peut imaginer que la methode qui fait la reconnaissance au lieu de retourner un objet qui peut contenir nimporte quel type d'information peut envoyer ce qu'elle vient de r\u00e9cup\u00e8rer dans une base de donn\u00e9e ou un objet. Par exemple une Zone de pilote pourrait tr\u00e8s bien contenir un objet pilote et le donner \u00e0 ses windows qui rempliraient ce m\u00eame objet. C'est une reflexion plus stockage que OCR mais c'est int\u00e9ressant pour savoir ce que fait une window des donn\u00e9es qu'elle r\u00e9cup\u00e8re. Dans un premier temps je pense que les windows vont simplement \u00e9crire dans un fichier ce qu'elles trouvent chacunes dans le format qu'elles veulent. Pour comprendre pourquoi je me prend la t\u00eate il faut savoir que chaque window peut avoir acc\u00e8s \u00e0 pleins d'informations diff\u00e9rentes. On pourrait dire qu'elles retournent toutes une string sauf que si ca marche pour un temps au tour ou pour un nom de pilote, cela ne marche pas forc\u00e9ment pour un type de pneu ou un DRS ouver. Comme chaque window a plusieurs types de data elle devra elle m\u00eame se charger de comment la traiter ET de la stocker. Voila un diagramme qui r\u00e9sume comment je vois l'impl\u00e9mentation dans un premier temps : \"Diagramme d'explications\" Voici comment se pr\u00e9sente le squellette d'une Zone : public class Zone { private Bitmap FullImage; private List Zones; private List Windows; private Rectangle _bounds; public Rectangle Bounds { get => _bounds; private set => _bounds = value; } public Bitmap ZoneImage { get { Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(FullImage, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); return sample; } } public Zone(Image fullImage, Rectangle bounds) { FullImage = (Bitmap)fullImage; Init(bounds); } public Zone(Bitmap fullImage, Rectangle bounds) { FullImage = fullImage; Init(bounds); } private void Init(Rectangle bounds) { Bounds = bounds; Zones = new List(); Windows = new List(); } public void AddZone(Rectangle bounds) { if(Fits(bounds)) Zones.Add(new Zone(ZoneImage,bounds)); } public void AddWindow(Rectangle bounds) { if (Fits(bounds)) Windows.Add(new Window(ZoneImage,bounds)); } private bool Fits(Rectangle inputRectangle) { if (inputRectangle.X + inputRectangle.Width > Bounds.Width || inputRectangle.Y + inputRectangle.Height > Bounds.Height || inputRectangle.X < 0 || inputRectangle.Y < 0) { return false; } else { return true; } } } Le but est ensuite de cr\u00e9er diff\u00e9rent types de Zones. Par exemple la MainZone devra d\u00e9couper son contenu en 20 parties \u00e9gales pour tenter de chopper les 20 pilotes. Il serait cool de trouver un moyen de calibrer automatiquement. C'est peut-\u00eatre possible de calibrer avec de la reconnaissance de texte, on peut essayer de lancer la reconnaissance et voir ou on trouve du texte avec un peu de chance cela pourrait donner les positions et avec ca on peut peut-\u00eatre determiner des lignes. Et voici le squelette d'une window g\u00e9n\u00e9rique using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace OCR_tester { public class Window { private Bitmap FullImage; private Rectangle _bounds; public Rectangle Bounds { get => _bounds; private set => _bounds = value; } public Bitmap WindowImage { get { Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(FullImage, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); return sample; } } public Window(Bitmap fullImage, Rectangle bounds) { FullImage = fullImage; Bounds = bounds; } public virtual void RecoverInformations() { //Each Window type will have to implement its own way to recover the informations stored in the Window Image } } } Chaque Window pourra ainsi elle m\u00eame impl\u00e9menter la r\u00e9cup\u00e8ration d'informations. La facon de les retourner/stocker est encore un peu floue. Par exemple pour un temps au tour on peut imaginer que il fait une petite v\u00e9rification dans l'objet pilote et dans le tableau des tours si il n'y a pas deja une valeur et si il n'y en a pas une alors il peut l'ajouter. Maintenant je vais essayer de cr\u00e9er une Main window qui se calibre toute seule. Alors apr\u00e8s avoir bien gal\u00e8r\u00e9 avec l'interface pour permettre au user de cliquer sur la form pour voir les zones qu'il cr\u00e9e, j'ai pu cr\u00e9er un zone qui fait les dimensions de MainZone et j'ai pu lancer la reconnaissance sur l'image et voir ou il trouve du texte : \"MainZone avec carr\u00e9s de texte\" Maintenant il faut que je nettoie la liste de rectangle pour exclure ceux qui sont trop grands pour \u00eatre sur une seule ligne, ceux qui indiquent le nombre de tour en haut et ceux qui n'ont pas d'int\u00e9r\u00eats. On pourra ensuite isoler les lignes et cr\u00e9er une liste d'images. Pour ce qui est de la ligne qui contient les \"Gap interval last lap\" et des chiffres sur les tours pour les pneus etc je vais juste demander \u00e0 l'utilisateur de ne pas les prendre dans la screenshot. Comme ils contiennent des mots qui peuvent \u00eatre utilis\u00e9s plus loin dans les data je ne peux pas les blacklister et faire un syst\u00e8me qui s'occupe de les enlever si ils existent selon le position y me prendrait trop de temps pour rien. Apr\u00e8s avoir filtr\u00e9 un peu les resultats et enlev\u00e9 les zones beaucoup trop grandes, on se retrouve d\u00e9ja plus qu'avec ca : \"MainZone avec de meilleurs carr\u00e9s\" Comme on peut le voir, du c\u00f4t\u00e9 gauche de l'image on a beaucoup de choses reconnues mais avec beaucoup de tailles diff\u00e9rentes ce qui n'est pas id\u00e9al. Alors j'ajoute un filtre qui permet de ne selectionner que les data sur la droite. \"MainZone avec de meilleurs carr\u00e9s\" Maintenant il devrait \u00eatre possible de faire un algorythme qui ne prend que un seul carr\u00e9 par ligne. \"MainZone avec un seul carr\u00e9 par ligne\" Maintenant que on sait ou se trouve chaque ligne on peut faire un petit traitement et d\u00e9couper l'image en plusieurs windows. Et voila : \"Mainzone auto calibr\u00e9e\" Maintenant le programme peut cr\u00e9er des zones pour chaque pilote \"Images pilotes\" \"Zone d'un pilote\" Maintenant il faut que j'impl\u00e9mente un syst\u00e8me un peu similaire pour cr\u00e9er des windows. Voici la methode que j'ai cr\u00e9\u00e9 pour l'autocalibration : public void AutoCalibrate() { List detectedText = new List(); Zones = new List(); TesseractEngine engine = new TesseractEngine(Window.tessDataFolder.FullName, \"eng\", EngineMode.Default); Image image = ZoneImage; var tessImage = Pix.LoadFromMemory(Window.ImageToByte(image)); Page page = engine.Process(tessImage); using (var iter = page.GetIterator()) { iter.Begin(); do { Rect boundingBox; if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox)) { //var text = iter.GetText(PageIteratorLevel.Word).ToUpper(); //We remove all the rectangles that are definitely too big if (boundingBox.Height < image.Height / NUMBER_OF_DRIVERS) { //Now we add a filter to only get the boxes in the right because they are much more reliable in size if (boundingBox.X1 > image.Width / 2) { //Now we check if an other square box has been found roughly in the same y axis bool match = false; //The tolerance is roughly half the size that a window will be int tolerance = (image.Height / NUMBER_OF_DRIVERS) / 2; foreach (Rectangle rect in detectedText) { if (rect.Y > boundingBox.Y1 - tolerance && rect.Y < boundingBox.Y1 + tolerance) { //There already is a rectangle in this line match = true; } } //if nothing matched we can add it if(!match) detectedText.Add(new Rectangle(boundingBox.X1, boundingBox.Y1, boundingBox.Width, boundingBox.Height)); } } } } while (iter.Next(PageIteratorLevel.Word)); } foreach (Rectangle Rectangle in detectedText) { Rectangle windowRectangle; Size windowSize = new Size(image.Width, image.Height / NUMBER_OF_DRIVERS); Point windowLocation = new Point(0, (Rectangle.Y + Rectangle.Height / 2) - windowSize.Height / 2); windowRectangle = new Rectangle(windowLocation, windowSize); Zones.Add(new Zone(ZoneImage, windowRectangle)); } } Ca peut paraitre pas \u00e9norme comme code mais pour tout mettre en place ca demande quand m\u00eame pas mal de reflexion. J'ai du clean un peu le code que j'avais fait pour permettre la selection de zones et ajouter la possibilit\u00e9 d'ajouter des windows sur une zone. J'ai juste quelques difficult\u00e9es \u00e0 les ajouter correctement, j'ai un offset tout pourri qui se met tout le temps \"Sainz coup\u00e9\" \"Perez coup\u00e9\" Cela doit \u00eatre un soucis lors de la detection de clic qui met un offset en trop. C'est vraiment p\u00e9nible en tout cas. Certes c'est moins fun de devoir manuellement indiquer ou sont les windows sur une ligne de pilote, mais je ne vois vraiment pas comment faire cela automatiquement. Le but c'est de faire une configuration qui puisse \u00eatre sauvegard\u00e9e comme ca pas besoin d'\u00e0 chaques fois le refaire. C'est bon ! J'avais juste oubli\u00e9 de changer le calcul d'offset entre le code de la zone et de la window. Note pour plus tard, il serait peut-\u00eatre judicieux de faire quelque chose pour la vue, les windows et les Zones ont le m\u00eame exact comportement pour la vue ce qui fait dupliquer du code. Mais au moins maintenant ca fonctionne : \"Ocr tester screenshot\" Et le programme va directement cr\u00e9er un dossier par pilote avec toutes les images de chaque Data le concernant : \"Dossier Perez\" ; Et c'est tout pour aujourd'hui je pense. Ce qui serait cool demain c'est que je puisse stocker d'une mani\u00e8re ou d'une autre ces fichiers de calibration et que je puisse les transf\u00e8rer vers le programme qui va s'occuper de d\u00e9coder et commencer gentillement \u00e0 d\u00e9coder les diff\u00e9rents types de data. Note pour quand je ferai les tests. Je pense que la meilleure id\u00e9e serait que je prenne pleins de photos du style et que je les mette dans un fichier CSV ou JSON avec leur contenu. Et ensuite je le fais passer en tests pour calculer la prescision de mon algo de d\u00e9codage. Pour le moment on est plut\u00f4t dans les clouts niveau planning.","title":"Lundi 3 Avril"},{"location":"jdb.html#mardi-4-avril","text":"Aujourd'hui je suis scens\u00e9 plut\u00f4t bosser sur l'interpretation des donn\u00e9es, mais une id\u00e9e m'a taraud\u00e9 l'esprit toute la nuit. Est-ce que je ne pourrais pas quand m\u00eame essayer de d\u00e9composer la zone de pilote directment comme pour la Main zone. Pour ce faire j'ai tent\u00e9 de faire comme pour la main zone c'est \u00e0 dire lancer la reconnaissance pour savoir ou \u00e9taient tous les champs de donn\u00e9es mais malheureusement je ne pense pas que cela va \u00eatre possible. En effet non seulement ici les champs sont de tailles tr\u00e8s vari\u00e9es, mais en plus la reconnaissance n'arrive pas \u00e0 en r\u00e9cup\u00e8rer le m\u00eame nombre sur chaque ligne ce qui risque d'\u00eatre complexe \u00e0 utiliser ensuite. La preuve : \"Tentative d'auto calibration\" ; Cependant tout n'est pas perdu ! Il y a peut-\u00eatre un moyen qui serait mieux en tous points. Le soucis avec ce type de reconnaissance c'est qu'on utilise beaucoup de ressources inutiles. On peut peut-\u00eatre hard coder la valeur des diviseurs et les utiliser pour cr\u00e9er des zones. Ok alors visiblement c'est un probl\u00e8me car il semble y avoir d'autres pixels de cette couleur dans l'image (Qui l'aurait cru lol) \"Tentative 2\" J'a tent\u00e9 de r\u00e9duire la tol\u00e9rance mais le soucis c'est que c'est soit trop soit pas assez Derni\u00e8re tentative, j'ai essay\u00e9 de prendre plusieurs pixels en hauteur pour chaque incr\u00e9ment de X et en faire la moyenne, et m\u00eame comme ca, impossible de trouver de mani\u00e8re efficace les zones. Je pense que je vais donc revert tous mes changements pour revenir \u00e0 la version ou on les choisissait manuellement. Pas mal de temps perdu mais bon c'est comme ca ca arrive Bon j'ai fait un revert mais j'ai ajout\u00e9 une feature importante. Les zones font la largeur indiqu\u00e9e par l'utilisateur mais elles font la hauteur max comme ca toutes les window font la m\u00eame hauteur et ca permet \u00e0 l'utilisateur de ne pas forc\u00e9ment \u00eatre ultra pr\u00e9cis dans sa selection. Ce qui nous donne : \"Resultat final\" Maintenant je dirais que les deux prochaines choses \u00e0 faire seraient de stocker ces zones dans un fichier JSON ou autre pour que la calibration puisse \u00eatre envoy\u00e9e directement dans le logiciel de reconnaissance et ensuite de faire une calibration sur des images qui font la taille qu'on aura pendant les Grands Prix. Pour le moment elles sont au format 16:10 qui est le format d'\u00e9crant de mon laptop. Pour le stockage j'imagine un fichier qui donne des indications assez simples qui permettent de reconstruire le total des zones quand il est import\u00e9 plutot que d'\u00e9crire les coordonn\u00e9es en dur pour chacunes. Chaque Grande zone va impl\u00e9menter une methode qui s'occupe de mettre tous ses enfants dans un fichier. { \"MainZone\":{ \"x\": 10, \"y\": 20, \"width\": 1450, \"height\": 1340, \"DriverZone\":{ \"x\": 0, \"y\": 23, \"height\": 25, \"Windows\":[ { \"DriverPositionWindow\":{ \"x\": 0, \"y\": 0, \"width\": 35 } }, { \"DriverPositionChangesWindow\":{ \"x\": 0, \"y\": 0, \"width\":45 } } ] } } } C'est le r\u00e9sultat auquel j'aimerais arriver. Mais pour y arriver il faut encore que je cr\u00e9e les diff\u00e9rents types de window. Cela veut dire que je dois d\u00e9cider quelles informations je vais r\u00e9cup\u00e8rer de la page. Par exemple je vais conserver la position du pilote mais au final les changements de positions sont difficiles \u00e0 lire et sont redondants. Si je garde un historique des positions des pilotes je peux calculer moi m\u00eame les changements. Pareil pour gap avec la voiture devant. Je pense que je vais juste garder l'information des \u00e9carts absolus et ensuite je pourrai toujours calculer la diff\u00e9rence entre les pilotes. Ca peut para\u00eetre b\u00eate car cela rajoute du calcul mais en r\u00e9alit\u00e9 le calcul de l'OCR est extr\u00eamement gourmand alors il faut que j'\u00e9vite le plus possible d'y faire recours. Il est bien plus rapide de calculer les \u00e9carts que d'essayer de reconnaitre le texte et le convertir en chiffre. J'ai visiblement ajout\u00e9 un bug dans mon code. Maintenant tous les pilotes ont la m\u00eame image quand on les selectionne. Mais visiblement ca n'\u00e9tait pas le cas avant car j'avais pu prendres des images de chaque pilote. J'ai pass\u00e9 3 minutes \u00e0 fixer un bug stupide j'ai un peu envie de br\u00fbler ma place de travail... Mais bon au moins maintenant cela fonctionne ! Toutes les images sont r\u00e9cup\u00e8r\u00e9es et ont un format correct avec le bon nom : \"Verstappen folder\" Avec un peu de code tr\u00e8s moche j'ai pu cr\u00e9er un fichier JSON qui contient les diff\u00e9rentes infos. Cependant en exportant TOUT on se retrouve avec un fichier de 1200 lignes ce qui n'est pas optimal. Mais quand on regarde, il devrait \u00eatre possible de faire un fichier qui ne contient que les infos d'un seul pilote car ensuite il y a simplement un offset \u00e0 appliquer sur la zone et les windows. Je vais donc pouvoir commencer enfin le logiciel de d\u00e9codage qui prend en entr\u00e9e un fichier JSON comme celui ci qui a \u00e9t\u00e9 g\u00e9n\u00e8r\u00e9 avec le programme de calibration. { \"Main\": { \"x\": 40, \"y\": 230, \"width\": 1845, \"height\": 719, \"Zones\": [ { \"DriverZone\": { \"x\": 0, \"y\": 3, \"width\": 1845, \"height\": 35, \"Windows\": [ { \"Position\": { \"x\": 2, \"y\": 0, \"width\": 32 }, \"GapToLeader\": { \"x\": 204, \"y\": 0, \"width\": 96 }, \"LapTime\": { \"x\": 413, \"y\": 0, \"width\": 105 }, \"Drs\": { \"x\": 526, \"y\": 0, \"width\": 81 }, etc... } ] } } ] } } Dans le futur il faudrait ajouter d'autres choses comme par exemple les diff\u00e9rents pilotes pr\u00e9sents sur le Grand Prix et ce genre d'infos. Quoique je vais l'ajouter d\u00e9ja maintenant et plus tard je mettrai en place la feature acessible depuis l'interface. Mais le hardcoder maintenant me permet d\u00e9ja de mieux coder l'autre c\u00f4t\u00e9. Ce programme n'est en aucun cas termin\u00e9 et je vais devoir travailler encore un peu dessus pour qu'il soit utilisable correctement mais au moins il fonctionne \u00e0 peu pr\u00e8s. Exemple du json avec les noms de pilotes: { \"Main\": { \"x\": 37, \"y\": 238, \"width\": 1851, \"height\": 713, \"Zones\": [ { \"DriverZone\": { \"x\": 0, \"y\": -5, \"width\": 1851, \"height\": 35 } } ] }, \"Drivers\": [ \"Leclerc\", \"Verstappen\", etc... ] } Maintenant je vais m'attaquer au d\u00e9codage. Demain je dois finir le d\u00e9codage du JSON et je dois commencer \u00e0 impl\u00e9menter la reconnaissance des textes. Voire m\u00eame des pneus etc si j'y arrive.","title":"Mardi 4 Avril"},{"location":"jdb.html#mercredi-5-avril","text":"Bon la il faut vraiment que je finisse assez vite la lecture du JSON et la reconstruction des zones pour commencer la reconnaissance. J'ai pris beaucoup de temps \u00e0 faire le programme de calibration mais je pense que c'est essentiel de prendre ce temps maintenant. (BTW il faudra quand m\u00eame retourner faire une plus jolie version par ce que la ca marche mais c'est tout) Bon apr\u00e8s pas mal de boulot je pense avoir r\u00e9ussi. Dans le nouveau programme on arrive \u00e0 r\u00e9cup\u00e8rer les diff\u00e9rentes zones : \"JSON decode result\" Un conseil de notre professeur M.Bonvin a \u00e9t\u00e9 de cr\u00e9er des Releases de versions qui ne fonctionnent pas ou pas tr\u00e8s bien. J'ai donc publi\u00e9 une premi\u00e8re release de l'OCR_TEST qui fonctionne vite fait. J'ai seulement un petit soucis, comme je recr\u00e9e compl\u00eatement la structure des driver zones avec seulement la premi\u00e8re, il y a un petit d\u00e9calage car entre les zones il y avait un gap. Ce qui fait que si la premi\u00e8re zone est parfaitement centr\u00e9e : \"Centered driver\" La vingti\u00e8me ne l'est plus exactement : \"Uncentered Driver\" Pour ca j'ai essay\u00e9 de mettre un espacement arbitraire mais c'est complexe. Je vais plut\u00f4t tenter de faire une diff\u00e9rence entre la taille de la zone compl\u00eate et de la taille additionn\u00e9e de toutes les fen\u00eatre et diviser le resultat entre toutes les fen\u00eatres. Ca n'est pas parfait mais au moins maintenant les donn\u00e9es ne touchent plus les bords de la fen\u00eatre. Et voila ! Maintenant avec le fichier de configuration en Json on arrive \u00e0 r\u00e9cup\u00e8rer toutes les infos comme si elles avaient \u00e9t\u00e9 envoy\u00e9es directement depuis l'app de calibration mais sans le processing time ! \"Verstappen folder 2 On peut donc ENFIN passer au d\u00e9codage de ces FICHUES donn\u00e9es. Je vais pouvoir impl\u00e9menter ce que j'ai fait dans le projet de test de d\u00e9codage. Gr\u00e2ce \u00e0 mon d\u00e9coupage initial qui m'a pris du temps \u00e0 impl\u00e9menter on a enfin un truc qui marche m\u00eame si je n'ai impl\u00e9ment\u00e9 que la reconnaissance de noms. \"Image reconnaissance propre\" Si on se rappelle du syst\u00e8me de window et de zones dans le diagramme plus haut, c'est assez facile de comprendre comment je m'y suis pris. En gros on des listes et des listes de listes de zones, c'est la partie un peu plus technique car il y a des zones qui peuvent contenir d'autres zones etc. Je vais commencer par la reconnaissance de noms. Voici le tableau de pilotes de 2023 \"Drivers\": [ \"Leclerc\", \"Verstappen\", \"Hamilton\", \"Alonso\", \"Russel\", \"Gasly\", \"Stroll\", \"Sainz\", \"Hulkenberg\", \"Norris\", \"Tsunoda\", \"Piastri\", \"Zhou\", \"Ocon\", \"Magnussen\", \"Perez\", \"Sargeant\", \"De Vries\", \"Bottas\", \"Albon\" ] ET voici le tableau de pilotes de 2022 : \"Drivers\": [ \"Leclerc\", \"Verstappen\", \"Sainz\", \"Perez\", \"Hamilton\", \"Russel\", \"Magnussen\", \"Gasly\", \"Ocon\", \"Alonso\", \"Tsunoda\", \"Bottas\", \"Zhou\", \"Albon\", \"Stroll\", \"Schumacher\", \"Hulkenberg\", \"Norris\", \"Latifi\", \"Ricciardo\" ] Je les notes ici car J'ai souvent besoin de changer selon le dataset que j'utilise. Dans le futur je ferai s\u00fbrement un grand dataset qui prend des pilotes de reserves et des pilotes juniors pour que dans le cas ou un pilote est remplac\u00e9 dans l'ann\u00e9e il n'y a pas besoin de tout recalibrer avec l'application. Apr\u00e8s une discussion avec M.Bonvin j'ai d\u00e9cid\u00e9 de tester 3 valeurs de convertion en noir et blanc et si je ne trouve pas un match exact je prend le nom le plus proche. Pour trouver la string la plus proche je pense que je vais utiliser quelque chose qui s'appelle la technique de Levenshtein. De ce que j'ai compris c'est un algorythme qui permet de donner une metric de diff\u00e9rence entre deux strings. Bon et \u00e9videmment il ne faut pas se tromper dans la liste des pilotes GENRE NE PAS OUBLIER QUE GEORGE RUSSELL COMPORTE DEUX WFNEWIEWV DE \"L\" A LA FIN DE SON NOM CE QUI POURRAIT ENGRANGER 2H DE DEBUGGING POUR RIEN ASK ME HOW I KNOW joker laugh J'ai vraiment un soucis avec Tsunoda, Il a trop tendeance \u00e0 le confondre avec \"TSUNDDA\" et pour des raisons obscures, quand j'applique l'algorythme de Levenshtein le plus proche n'est pas \"Tsunoda\" mais \"Sainz\" iniuvbwdiucbiubisc POURQUOI !!??!! Je pense que cela demande moins de changements de lettres enfin bon c'est quand m\u00eame pas id\u00e9al. Il va falloir que je trouve un moyen de le repond\u00e9rer. C'est dommage par ce que cela marche super avec Alonso Verstappen et Albon. J'ai un peu modifi\u00e9 la methode et j'ai fait en sorte d'envoyer tous les noms en majuscules en me disant que cela pourrait r\u00e9duire le nombre de changements. Et ca a march\u00e9 !! Cela va s\u00fbrement demander plus de tests pour \u00eatre bien s\u00fbr que tout fonctionne nikel, cependant pour le moment ca marche parfaitement avec les pilotes de 2022. Pour ce qui est de la reconnaissance de chiffres, j'ai d\u00e9ja fait une partie du boulot le premier jour alors je vais juste reprendre \u00e0 partir de l\u00e0. Je r\u00e9cup\u00e8re une string de ce type \"1:35.123\" le soucis c'est que les : se transforment parfois en . ou inversement mais bon ca devrait pas \u00eatre trop dur \u00e0 g\u00e8rer. Il faut que je transforme cette string en nombre de milisecondes (Du moins je pense que c'est le meilleur moyen pour ensuite pouvoir facilement comparer et stocker l'information). Cela fait que 1:35:123 en milisecondes donne : 1 * 1000 * 60 => 60'000 35 * 1000 => 35'000 123 => 123 Total : 60'000+35'000+123 => 95'123ms Et pour l'affichage : Minutes = ms / 60'000 secondes = (ms - (minutes/60'000))/1000 ms = ms - ((minutes 60'000) + (secondes * 1000)) Et on se retrouve avec 1:35:123 Maintenant apr\u00e8s un peu de temps pour nettoyer la string etc on se retrouve avec un r\u00e9sultat comme le suivant : Position : 0 Gap to leader : 0:0:0 Lap time : 2:15:123 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : LECLERC Sector 1 : 0:31:323 Sector 2 : 0:42:340 Sector 3 : 0:0:0 Evidemment pareil pour les autres pilotes Et je me rend compte que j'ai encore tout cass\u00e9 car le laptime ne devrait pas \u00eatre 2:15 mais 1:35... Voila apr\u00e8s une heure de debugging et des ajouts pour nettoyer les chaines on se retrouve avec : Position : 0 Gap to leader : 0:0:0 Lap time : 1:35:123 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : LECLERC Sector 1 : 0:31:323 Sector 2 : 0:42:340 Sector 3 : 0:0:0 Note: le traitement commence \u00e0 devenir long, il serait peut-\u00eatre int\u00e9ressant d'utiliser un seul Tesseract Engine ou de voir ce qui prend autant de temps, on d\u00e9passe la seconde de traitement ce qui est un peu ma limite. Apr\u00e8s on peut toujours tester de rajouter du multicore processing mais c'est pour une autre fois. Demain je m'occupe de r\u00e8gler les soucis que j'ai avec la prescision de ces temps au tour et j'\u00e9sp\u00e8re pouvoir m'occuper aussi de la position des pneus et du DRS. J'aimerais finir tout ca cette semaine.","title":"Mercredi 5 Avril"},{"location":"jdb.html#jeudi-6-avril","text":"Une id\u00e9e m'est pass\u00e9e par la t\u00eate pendant que je dormais, dans la liste des pilotes, quand ils sont \u00e0 plus d'un tour de retard avec le leader (Ce qui arrive normalement dans presque tous les Grand Prix) on a pas des minutes mais une string qui montre \"+1 Lap\" ou \"+2Laps\" ce qui est \u00e9videmment un probl\u00e8me. Je pense qu'une bonne facon d'envoyer l'info serait de retourner -1 -2etc... \u00e0 la place des milisecondes, mais encore faut-il detecter le nombre de tours Je devrais \u00eatre en train de commencer la documentation de commment tout ce que j'ai fait fonctionne. Cependant je ne me vois pas faire ca tant que je n'ai pas au moins r\u00e9cup\u00e8r\u00e9 toutes les infos au moins un peu proprement. Cela veut dire que je commence officiellement \u00e0 prendre du retard. (Sachant que si je finis tout aujourd'hui une journ\u00e9e de doc suffira largement le terme est un peu exag\u00e8r\u00e9 mais bon) Bon pour la reconnaissance des temps c'est sp\u00e9cial... Le filtre semble ne pas changer grand chose ce qui est probl\u00e9matique et ca n'est vraiment pas fiable. Voici quelques expemples avec un treshold de 100: \"11ZSD\" Cette image est comprise comme \"11ZSD\" 42340 Cette image est comprise comme \"42340\" \"ZZAEB\" Et celle ci \"ZZAEB\"... Ce qui... n'est pas bon du tout... J'ai essay\u00e9 de trouver un fichier d'entrainement sp\u00e9cifiquement fait pour les digits. J'ai essay\u00e9 de blacklister les chars non voulus pour tenter d'obliger Tesseract \u00e0 trouver des chiffres. Avec la premi\u00e8re option, les r\u00e9sultats ne sont pas meilleurs voire pires. Avec la seconde option c'est d\u00e9ja pas mal mieux mais on perd compl\u00e8tement la possibilit\u00e9 de detecter les mots comme \"LEADER\" ou \"LAP\" et de toute facon ca n'est pas parfait. Le soucis c'est que si je n'ai pas des donn\u00e9es fiables c'est juste impossible de faire des calculs et de l'affichage correct... Il faut absolument que je trouve une solution. J'ai essay\u00e9 d'utiliser de l'interpolation our augmenter la taille de l'image et ensuite appliquer mon filtre pour retirer le flou mais sans succ\u00e8s... Pourtant la on se retrouve avec des images plut\u00f4t claires : \"Clear1\" Ici le programme trouve \"44301\" \"Clear2\" Et ici \"A5151\"... On a toujours les m\u00eames probl\u00e8mes. Bon je suis all\u00e9 me renseigner sur l'OCR et je me suis dit que j'allais tenter de faire les choses proprement. Je vais faire passer plusieurs \u00e9tapes de postProcessing avant de donner l'image \u00e0 Tesseract. GrayScale Tresholding InvertColors Scaling Dilatation Ce qui donne : \"Original\" \"Grayscale\" \"InvertColors\" ; \"Resize\" ; \"Dilatation\" Ce qui ne change : Roulement de tambour RIEN kjd viuwvuirnvoirenbf Tout ca pour rien... C'EST BON !!! Bon en fait au final le probl\u00e8me \u00e9tait une mauvaise configuration de Tesseract. Je vais devoir un peu nettoyer tout ca. Mais avec les changements de l'image on a des r\u00e9sultats BEAUCOUP plus pr\u00e9cis et potentiellement utilisables. La je vais devoir faire un serieux travail de nettoyage et simplification de mon code par ce que la c'est vraiment un chantier vu le nombre de choses que j'ai du essayer. J'ai du aussi beaucoup modifier la gestion de l'image ce qui donne : \"Clean\" Et la on a des r\u00e9sultats qui sont vraiment bons. J'ai pu ajouter assez facilement la detection de position comme c'est simplement un chiffre. On se retrouve maintenant avec ce genre de retours : Position : 1 Gap to leader : 8:33:51 Lap time : 2:19:123 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : LECLERC Sector 1 : 0:31:828 Sector 2 : 0:42:940 Sector 3 : 0:0:0 Position : 2 Gap to leader : 0:3:259 Lap time : 23:12:392 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : VERSTAPPEN Sector 1 : 0:38:119 Sector 2 : 0:0:0 Sector 3 : 0:0:0 Il ne manque plus que l'impl\u00e9mentation de la reconnaissance du DRS et des Pneus Et non... je viens de me rendre compte que mon programme a encore cass\u00e9 car le tap time ne peut pas \u00eatre 23 min lol. J'ai un nouveau magnifique probl\u00e8me... Les points et les deux points sont interpr\u00eat\u00e9s comme des chiffres ... Give me a F * * break... J'ai du mal \u00e0 comprendre pourquoi ils ne sont d\u00e9tect\u00e9s comme tels que maintenant. Bon alors il semblerait les temps au tour aie besoin d'un ordre tr\u00e8s pr\u00e9cis pour fonctionner. Grayscale InvertColors Tresholding Resize * 2 Resize * 2 Et la on a des r\u00e9sultats un peu mieux. Bon demain il faut absolument que je me charge de r\u00e8gler tous ces probl\u00e8mes et que je commence la reconnaissance des pneus et de DRS par ce que je commence \u00e0 \u00eatre en retard.","title":"Jeudi 6 Avril"},{"location":"jdb.html#vendredi-6-avril-2023","text":"Alors aujourd'hui c'est le dernier jour avant de commencer \u00e0 \u00eatre en retard pour de bon. J'ai r\u00e9ussi \u00e0 r\u00e8gler le probl\u00e8me des temps au tour, des gaps, et des secteurs. Dans le processus j'ai cass\u00e9 la detection de position mais ca devrait pas \u00eatre TROP compliqu\u00e9. Et voila ... Apr\u00e8s seulement plus de dix heures de gal\u00e8re, si on donne cette image au programme et le bon JSON le programme nous retourne : Position : 1 Gap to leader : 0:05:059 Lap time : 1:39:123 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : LECLERC Sector 1 : 0:31:828 Sector 2 : 0:42:940 Sector 3 : 0:00:000 Position : 2 Gap to leader : 0:03:259 Lap time : 1:39:392 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : VERSTAPPEN Sector 1 : 0:31:749 Sector 2 : 0:00:000 Sector 3 : 0:00:000 Evidemment le GapToLeader est faux sur leclerc car il est leader mais bon ca je pourrai toujours Hardcoder que le premier a jamais de GapToLeader. Bon j'ai eu beaucoup de soucis que je ne vais pas mentionner ici car ce sont simplement des soucis de logique de programmation pour trouver un DRS ouvert ou non. Au final la technique que j'utilise et qui marche plut\u00f4t bien pour le DRS est que je prend la premi\u00e8re image de DRS et je la d\u00e9clare comme valeur \u00e9talon d'un DRS non actif, en effet dans 99% des cas le leader n'a pas de DRS (cela peut arriver alors il faudra donc juste verifier que les pilotes sont bien \u00e0 moins de deux secondes les uns des autres pour confirmer). Ensuite cette valeur \u00e9talon je la calcule en fonction du nombre de pixels verts dans l'image et si il y a plus de 30% de pixels verts en plus c'est que le DRS est activ\u00e9 ex: Ceci est un DRS ferm\u00e9: \"Closed DRS\" Ceci est un DRS ouvert: \"Open DRS\" Cela marche \u00e0 peu pr\u00e8s tout le temps mais dans le pire des cas on peut toujours verifier que les pilotes sont bien proches pour detecter les potentiels rares cas de faux positifs. J'ai pu augmenter les performances en utilisant un seul engine pour tout le monde et en arr\u00eatant d'utiliser GetPixel et SetPixel qui sont simplement des horreurs \u00e0 utiliser. Mais elles ne sont pas encore bonnes Le soucis avec la detection de pneus cependant, c'est qu'il n'est pas possible d'utiliser la reconnaissance pour savoir ou regarder la couleur car cela ne marcherait pas. Je ne peux pas faire trop de post processing car je dois conserver la couleur Je ne peux pas hardocder un endroit ou aller regarder car cela \u00e9volue tout le long du Grand Prix. Bref c'est la gal\u00e8re. En y r\u00e9flechissant je me suis dit qu'une bonne id\u00e9e pourrait \u00eatre de partir de la droite de la zone du pneu en regardant au milieu de la hauteur. Puis continuer vers la gauche jusqu'\u00e0 ce que je rencontre une couleur diff\u00e9rente. Je pourrai ensuite faire une zone un peu vers la gauche qui devrait contenir les infos du pneu et sur laquelle il sera possible de faire de le reconnaissance de couleur et de la reconnaissance de chiffres. J'ai d\u00e9termin\u00e9 que le background n'\u00e9tait jamais plus clair que #505050 et que donc nimporte quelle couleur qui aurait plus que 50 dans un seul des channels serait consid\u00e8r\u00e9e comme une couleur cassant le background Pour arriver \u00e0 cette conclusion je me suis amus\u00e9 un peu avec les couleurs pour jouer avec les limites de mon algorythme : \"Color fun\" Et je crois que j'ai eu une bonne id\u00e9e, avec une petite methode bien faite on arrive \u00e0 de supers r\u00e9sultats : private Rectangle FindTyreZone() { Bitmap bmp = WindowImage; int currentPosition = bmp.Width; int height = bmp.Height / 2; Color limitColor = Color.FromArgb(0x50,0x50,0x50); Color currentColor = Color.FromArgb(0,0,0); Size newWindowSize = new Size(bmp.Height,bmp.Height); while(currentColor.R <= limitColor.R && currentColor.G <= limitColor.G && currentColor.B <= limitColor.B && currentPosition > 0) { currentPosition--; currentColor = bmp.GetPixel(currentPosition,height); } //Its here to let the new window include a little bit of the right side int offset = Convert.ToInt32((float)newWindowSize.Width / 100f * 20f); int CorrectedX = currentPosition - (newWindowSize.Width - offset); if (CorrectedX <= 0) return new Rectangle(0,0,newWindowSize.Width,newWindowSize.Height); return new Rectangle(CorrectedX,0,newWindowSize.Width,newWindowSize.Height); } \"Tyres\" Maintenant cela devrait \u00eatre beaucoup plus simple de trouver la couleur g\u00e9n\u00e9rale et le nombre de tours. Donc ce que je fais c'est que je fais une reconnaissance de texte sur l'image r\u00e9duite. Si je trouve une lettre c'est facile Ca me donne le type de pneu et ca me dit que c'est le premier tour avec. Si c'est un nombre alors je fais la moyenne de toutes les couleurs de l'image et je prend la couleur de pneu la plus proche. Voici les diff\u00e9rentes couleurs de pneus : SOFT : #FF0000 MEDIUM : #f5bf00 HARD : #d9d8d4 INTER : #00a42e WET : #2760a6 \"Tyre colors\" Les couleurs de pneus peuvent changer de temps \u00e0 autres, par exemple cette r\u00e8gle de pneus est arriv\u00e9e en 2019 et avant il y avait beaucoup plus de couleurs mais dans une volont\u00e9 de rendre le sport plus facile \u00e0 comprendre \u00e0 la t\u00e9l\u00e9 cela a \u00e9t\u00e9 simplifi\u00e9. Je ne pense pas que cela va changer dans les ann\u00e9es qui viennent alors tout est hardcod\u00e9. Je pense que j'ai des soucis avec la detection de texte et de couleur car ma zone est trop grande. Alors bon j'\u00e9crit ces lignes apres des heures de tests. Il semble que la principale difficult\u00e9 avec ces pneus c'est que les chiffres ou lettres sont minuscules. Il est donc extr\u00eamement difficile de faire une reconnaissance ne serait-ce qu'un peu fiable.. Je fais de mon mieux pour tenter de r\u00e8gler le soucis cependant c'est vraiment complexe. Je commence \u00e0 devenir fou, je tente tout et nimporte quoi pour permettre \u00e0 mon algo de fonctionner et m\u00eame quand je fais du post processing comme pas possible il me retourne toujours nimporte quoi... \"5i t'inqui\u00e8tes\" Ici le programme va trouver '5i'... En fait c'est complexe d'expliquer tout ce que je fais car je change tout en boucle en essayant et en ratant ce qui prend des heures. Pour aujourd'hui j'abandonne je vais simplement rentrer chez moi et y r\u00e9flechir cette nuit mais je ne vois pas comment mieux faire la... C'est terrible par ce que je sens que je ne suis pas bien loin.","title":"Vendredi 6 Avril 2023"},{"location":"jdb.html#vacances","text":"Bon je vais un peu laisser de c\u00f4t\u00e9 la detection de chiffres pour me pencher un peu plus sur la d\u00e9tection de couleur. Par ce que techniquement si j'arrive \u00e0 toujours parfaitement la detecter alors je pourrais me passer des chiffres car ils sont redondant si je construit un historique de pneus. J'ai r\u00e9ussi \u00e0 fix mon probl\u00e8me de mauvaise detection de couleur de pneus. Du moins je crois. Seulement j'ai quand m\u00eame un souci, les fen\u00eatres de pneus avec une lettre n'ont pas assez de couleur pour \u00eatre d\u00e9tect\u00e9s. Je vais donc essayer de detecter les cinq lettres possibles et si il ne trouve pas alors je pourrai tenter de detecter les chiffres sans lettres ce qui devrait grandement aider. Le but est encore une fois de r\u00e9duire les possibilit\u00e9s de Tesseract. Je me rend de plus en plus compte que le plus important c'est de r\u00e9duire le scope le plus possible. Moins il y a de mots et lettres et de chiffres possibles meilleure sera la reconnaissance. Bon ca ne veut toujours pas marcher maintenant le 11 est interpr\u00eat\u00e9 comme trois I ou comme un M... J'en ai marre sans rire c'est vraiment p\u00e9nible. Alors j'\u00e9crit ces lignes deux jours plus tard et me rend compte avec horreur que toutes mes modifications sur ce journal de bord n'ont pas \u00e9t\u00e9 auvegard\u00e9e... yess.. Bon pour faire simple, j'ai r\u00e9ussi \u00e0 rendre la detection de couleurs bien plus efficace en r\u00e9duisant la taille de l'image et en ne prenant pas en compte les couleurs que l'on d\u00e9tecte comme \u00e9tant partie int\u00e9grante du background. Par exemple quand on a une image comme celle ci : \"Avec background\" qui contient un background alors que ci dessous, on l'a enlev\u00e9. \"Sans background\" La diff\u00e9rence est t\u00e9nue mais elle permet de grandement am\u00e9liorer la prescision de la reconnaissance de couleurs. Pour ce qui est du nombre de tours je me suis rendu compte que cela n'\u00e9tait d\u00e9ja pas tr\u00e8s utile car avec l'historique on devrait pouvoir le d\u00e9duire. Mais bon pour la forme je me suis dit que cela serait quand m\u00eame une bonne id\u00e9e de v\u00e9rifier avec la reconnaissance. J'\u00e9tais quasi certain que le soucis \u00e9tait le fait que l'on voie le contour du logo de pneu qui faisait que la reconnaissance avait du mal. Et j'avais raison ! En les enlevant (Ce qui n'a pas \u00e9t\u00e9 simple) J'ai pu avoir des chiffres beaucoup plus proches de la r\u00e9alit\u00e9. En m\u00eame temps je ne vois pas bien comment j'aurais pu faire mieux : \"Super 11\" Je suis quand m\u00eame assez fier de voir que j'ai r\u00e9ussi \u00e0 part de l'image que on peut voir un peu plus haut et automatiquement la transormer en celle ci-dessus. J'ai donc pu retirer le round autour du chiffre et cela m'a permit de pouvoir d\u00e9zoomer un peu et c'est avec ca que les lettres ont pu \u00eatre mieux reconnues : \"Super H\" \"Super M\" Maintenant je pense qu'il ne reste \"plus qu'\u00e0\" nettoyer un peu tout ce code qui traine de partout pour tout faire fonctionner et impl\u00e9menter un peu de parrallel processing ainsi que de l'asynchrone pour ne pas bloquer le reste du programme. Par ce qu'il faut savoir que en l'\u00e9t\u00e2t, le programme met 25 secondes \u00e0 d\u00e9marrer et consomme presque 2GB de Ram. Certes cela ne veut pas dire que la reconnaissance \u00e0 elle seule prend 25 secondes car au d\u00e9marrage il y a aussi la lecture du fichier de config et la cr\u00e9ation des window etc.. En r\u00e9alit\u00e9 la partie strictement OCR prend dans les 12s si on en croit la fonction stopWatch de C#. Et quand on change d'image la reconnaissance prend 9s. Dans tout les cas c'est BEAUCOUP trop. J'aurais eu comme objectif de faire une reconnaissance toutes les secondes. Je ne sais pas bien si cela va \u00eatre possible mais en tout cas le but va \u00eatre de s'en rapprocher. Pour \u00eatre plus exact et permettre une comparaison, voici les stats exactes Avec un fichier d'images vide : Loading - 11.8s Splitting d'images - 90ms OCR - 12.5s Avec un fichier d'images plein : Loading - 10.8s Splitting d'images - 80ms Ocr - 11.6s En passant d'une image \u00e0 l'autre : Loading - NaN Splitting d'images - 50ms Ocr - 8.8s Donc on peut voir que les deux endroits ou le programme prend le plus de temps c'est au premier d\u00e9marrage quand il faut lire le fichier et setup les windows etc... Et l'OCR qui prend un temps fou. Ce qui est pratique c'est que les presque 2gb de ram sont utilis\u00e9s que au lancement et ensuite l'application n'en utilise que quelques centaines de mb. Le processeur lui tourne entre 10 et 20% ce qui ne va pas durer :) Je vais m'occuper dabord du loading. J'ai essay\u00e9 d'utiliser un Parrallel.For au moment de la cr\u00e9ation des windows, le probl\u00e8me c'est que visiblement les objets windows sont beaucoup trops complexes et utilisent trop de ressources partag\u00e9es pour \u00eatre vraiment thread Safe. J'\u00e9sp\u00e8re que je n'aurais pas trop de soucis avec ca qu'en j'en viendrai \u00e0 l'optimisation de l'OCR... Ce qui me rend fou c'est que cette boucle toute nulle prend plus de dix secondes \u00e0 s'executer et je ne comprend pas bien pourquoi. for (int i = 0; i < NUMBER_OF_DRIVERS; i++) { Point tmpPos = new Point(0, FirstZonePosition.Y + i * FirstZoneSize.Height - Convert.ToInt32(i * offset) /*- (i* (FirstZoneSize.Height / 32))*/); Zone newDriverZone = new Zone(MainZoneImage, new Rectangle(tmpPos, FirstZoneSize)); Bitmap zoneImg = newDriverZone.ZoneImage; newDriverZone.AddWindow(new DriverPositionWindow(zoneImg, new Rectangle(driverPositionPosition, driverPositionArea))); newDriverZone.AddWindow(new DriverGapToLeaderWindow(zoneImg, new Rectangle(driverGapToLeaderPosition, driverGapToLeaderArea))); newDriverZone.AddWindow(new DriverLapTimeWindow(zoneImg, new Rectangle(driverLapTimePosition, driverLapTimeArea))); newDriverZone.AddWindow(new DriverDrsWindow(zoneImg, new Rectangle(driverDrsPosition, driverDrsArea))); newDriverZone.AddWindow(new DriverTyresWindow(zoneImg, new Rectangle(driverTyresPosition, driverTyresArea))); newDriverZone.AddWindow(new DriverNameWindow(zoneImg, new Rectangle(driverNamePosition, driverNameArea))); newDriverZone.AddWindow(new DriverSector1Window(zoneImg, new Rectangle(driverSector1Position, driverSector1Area))); newDriverZone.AddWindow(new DriverSector2Window(zoneImg, new Rectangle(driverSector2Position, driverSector2Area))); newDriverZone.AddWindow(new DriverSector3Window(zoneImg, new Rectangle(driverSector3Position, driverSector3Area))); MainZone.AddZone(newDriverZone); } Alors que Zone.AddWindow c'est simplement : public virtual void AddWindow(Window window) { Windows.Add(window); } Et windows est simplement une liste. Donc ca ne peut pas \u00eatre ca qui prend du temps. Et les windows que je cr\u00e9\u00e9 ont ca comme code : public DriverPositionWindow(Bitmap image, Rectangle bounds) : base(image, bounds) { Name = \"Position\"; } Sachant que le constructeur de base d'une Window c'est : public Window(Bitmap image, Rectangle bounds) { Image = image; Bounds = bounds; Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, \"eng\", EngineMode.Default); Engine.DefaultPageSegMode = PageSegMode.SingleLine; } Sachant que TesseractEngine est en statique et que donc il ne devrait... OHLLALALALALALALALALA je suis un imb\u00e9cile... J'ai juste \u00e0 changer ce constructeur avec ca: if (Engine == null) { Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, \"eng\", EngineMode.Default); Engine.DefaultPageSegMode = PageSegMode.SingleLine; } ET le loading ne prend plus que 2-300 ms... Bon c'est une tr\u00e8s belle am\u00e9lioration pour pas tr\u00e8s ch\u00e8r mais bon c'est un peu b\u00eate... Bon je pense que 2-300ms c'est une dur\u00e9e correcte surtout que ca n'est appel\u00e9 qu'une fois pour le lancement. On peut passer \u00e0 la suite maintenant. Alors il y a un grand soucis avec la parallellisation de l'OCR... Tesseract n'est pas par d\u00e9faut une classe \"Thread safe\" ce qui veut dire que je ne peut utiliser de parallell.Foreach sur mes windows pour acc\u00e8l\u00e8rer le traitement drastiquement. Je pourrais par exemple avoir une instance de Tesseract par window sauf que cela fait 20 pilotes * 9 windows chacuns ce qui donne 180 instances ce qui n'est tout simplement pas raisonnable. Je vais donc essayer de voir avec l'utilisation de methodes asynchrones qui me permettraient de faire un genre de flux tendu de reconnaissance. J'avoue que la je navigue un peu \u00e0 vue, je me base sur diff\u00e9rentes infos que je trouve sur des sites un peu perdus et sur chatGPT, j'esp\u00e8re que j'arriverai \u00e0 trouver une solution car 10 secondes de reconnaissance c'est vraiment beaucoup trop. Alors le soucis avec un Engine unique entre toutes les windows c'est qu'il n'est pas possible de process plusieurs images \u00e0 la fois. Je vais donc retirer l'engine unique pour voir si en cr\u00e9er un par window me permet de passer en multithreading. La grande question sera : Est-ce que les ressources suppl\u00e9mentaires que vont prendre la cr\u00e9ation de tous ces engines va compenser enti\u00e8rement le temps gagn\u00e9 avec la paralellisation. Pour stocker les donn\u00e9es dans un premier temps je vais cr\u00e9er un objet DriverData. Ce qu'il y a de pratique avec ca, c'est que je pourrais ajouter du code de v\u00e9rification de certaines donn\u00e9es directement dedans avant de les donner \u00e0 la suite du programme. Et on peut m\u00eame imaginer une impl\u00e9mentation d'une liste de DriverData pour avoir l'historique. Ce qui serait cool ca serait de grouper toutes ces data avec un num\u00e9ro de tour. Placer ensuite la liste de Data dans une DB serait ainsi super simple. Mais il va falloir savoir quoi mettre, quelles infos sont redondantes et prendre en compte le fait que un tour affich\u00e9 sur la page de la F1TV n'est accompli que par certains des premiers pilotes. D'autres pilotes peuvent \u00eatre dans des tours pr\u00e9c\u00e9dents si ils ont du retard. Il faudra r\u00e9fl\u00e9chir \u00e0 cela quand je viendrai au mod\u00e8le. Bon pour y arriver j'ai du faire de gros changements et le r\u00e9sultat n'est peut-\u00eatre pas aussi cool que ce que j'aurais voulut... Voici un petit point sur les performances maintenant J'ai \u00e9galement d\u00e9sactiv\u00e9 le dump d'images. Pour le moment j'ai tout mis en commentaire mais cela pourrait \u00eatre int\u00e9ressant de faire en sorte de pouvoir l'activer en changeant une ou deux variables Au d\u00e9marrage : Loading - 113ms Splitting d'images - 14ms Ocr - 7s En passant d'une image \u00e0 l'autre : Loading - 113ms Splitting d'images - 13ms Ocr - 5s Alors clairement les stats montrent qu'il y a eu un changement mesurable mais bon je pensais pouvoir en gagner un peu plus... Je soupconne la cr\u00e9ation d'engines d'\u00eatre \u00e0 l'origine de ces performances presque d\u00e9cevantes. Autre soucis, il semble que plus je change d'image plus la detection est lente et plus je consomme de RAM. Il va falloir que je travaille encore un peu. J'ai tent\u00e9 de mettre un stopwatch sur une des cr\u00e9ations d'engine Tesseract et le r\u00e9sultat me parait fou... Plus d'une seconde c'est dingue. J'ai test\u00e9 dans d'autres endroits du code et effectivement il semble que la cr\u00e9ation d'un engine prenne entre une et deux secondes ce qui est une ETERNITEE what ! Donc il faut optimiser tout ca. Une id\u00e9e serait de d\u00e9composer le threading mais cela me demanderait un gros refactor et je n'ai pas envie d'en refaire un la... Sinon, une fois qu'ils sont cr\u00e9\u00e9s ils ne prennent pas de temps du tout. Cr\u00e9er une fois tous les engines et ensuite les utiliser pourrait \u00eatre une bonne id\u00e9e. Cela prendrait longtemps au load mais ensuite les reconnaissances devraient \u00eatre super rapides. Ok alors ca c'est d\u00e9ja plus ce \u00e0 quoi je m'attendais ! On est de nouveau \u00e0 plus de 10s de loading time mais on est descendu \u00e0 deux secondes par OCR. (Bon autre soucis, l'utilisation de la RAM est ridicule plus de 2gb mais ce qui m'inqui\u00e8te c'est que j'ai l'impression qu'elle augmente plus on change d'image) J'ai r\u00e8gl\u00e9 (en partie) le soucis en obligeant le GC (Garbage Collector) \u00e0 collecter apr\u00e8s chaque detection. m\u00eame apr\u00e8s 50 detections l'utilisation de la ram se stabilize autour des 2GB. Bon en paralellisant la cr\u00e9ation des Engines le soucis c'est que cela demande d'allouer beaucoup trop de m\u00e9moire d'un coup alors le programme se fige pendant genre cinq secondes avant de tout cr\u00e9er. Du coup m\u00eame si la cr\u00e9ation est plus rapide, on se retrouve avec un temps total plus long... Je pense que l'on va devoir se contenter de ces dix secondes. Bon la j'allais tenter de faire la documentation mais je viens de me rendre compte que la detection de temps au tour est pas vraiment encore id\u00e9ale... J'ai r\u00e9ussi \u00e0 changer un petit peu le programme de reconnaissance pour rendre la reconnaissance un peu meilleure mais cela a drastiquement augment\u00e9 le temps requis pour d\u00e9coder... On arrive \u00e0 3.5 secondes. Je vais tenter de rajouter un peu de parralell processing sur les boucles de traitement voir si cela peut aider. Alors effectivement cela aide pas mal, on arrive maintenant \u00e0 faire une detection presque tout le temps en dessous de la seconde. Et j'ai aussi du changer un peu le fonctionnement de la detection des Temps au tour. Et voila je pense que je vais m'arr\u00eater la pour la partie d\u00e9codage. Je ne pense pas que je peux facilement faire mieux que ca et il faut que j'avance dans d'autres parties du projet. Je vais pouvoir commencer \u00e0 documenter un peu toute la partie OCR. Il faut que je prenne le temps de le faire bien car c'est la partie la plus int\u00e9ressante du projet et ou je pense que j'aurai le plus essay\u00e9 de choses qui vallent le coup d\u00eatres racont\u00e9es. J'ai aussi pass\u00e9 pas mal de temps sur le poster du projet. J'avais fait des croquis au crayon de ce \u00e0 quoi je pensais, cependant apr\u00e8s de longues discussions avec M.Garcia ils n'\u00e9taient pas forc\u00e9ment tr\u00e8s bons car ils ne repr\u00e9sentent pas assez bien le fonctionnement du projet et sont un peu trop marketings. Du coup j'ai fait une premi\u00e8re version au propre : \"Poster V1\" Mais je n'\u00e9tais pas forc\u00e9ment content du r\u00e9sultat et il manquait des choses je trouve comme par exemple l'utilisation de Selenium. J'ai donc repass\u00e9 des heures \u00e0 faire une seconde version : \"Poster V2\" La police d'\u00e9criture n'est pas encore la bonne mais cela va venir. Mais je pr\u00e9f\u00e8re d\u00e9ja beaucoup cette version \u00e0 la premi\u00e8re. Je ne sais pas encore si la version finale sera une version plus travaill\u00e9e de ce poster ou compl\u00eatement autre chose mais pour l'instant je suis \u00e0 peu pr\u00e8s content de cette version. Je le trouve un tout petit peu trop brouillon ou avec trop d'infos mais il m'a \u00e9t\u00e9 de nombreuses fois reproch\u00e9 de ne pas assez montrer le fonctionnememt interne et je ne peux pas faire plus simple. L'ajout des nombres pour compartimenter le projet ajoute de la structure mais je me demande si cela suffit. Maintenant que je suis \u00e0 peu pr\u00e8s content de mon code pour l'OCR je vais commencer sa documentation. (Uniquement son fonctionnement interne pas comment s'en servir car cela va changer) Bon j'ai cr\u00e9\u00e9 u nouveau projet selenium mais m\u00eame avec les bonnes libraries je n'arrivais pas \u00e0 faire fonctionner firefox j'avais toujours une erreur \"OpenQA.Selenium.WebDriverException: 'Cannot start the driver service on http://localhost:51481/'\" et j'ai pu r\u00e8gler le probl\u00e8me en t\u00e9l\u00e9chargeant directement le gecko driver depuis le git https://github.com/mozilla/geckodriver/releases et utiliser le fichier directement dans le service : var service = FirefoxDriverService.CreateDefaultService(AppDomain.CurrentDomain.BaseDirectory+@\"geckodriver-v0.27.0-win32\\geckodriver.exe\"); FirefoxOptions options = new FirefoxOptions(); var driver = new FirefoxDriver(service,options); Le seul probl\u00e8me c'est que du coup il faut tout le temps d\u00e9placer le fichier dans le dossier bin si je clone le projet. Il faudra faire un installeur dans la version finale qui s'occupe de tout je pense. Je me suis dit que j'allais garder la doc pour le retour des vacances quand j'aurai un bureau un clavier et un setup complet un peu propres. Bon il va falloir que je parle de la r\u00e9cup\u00e9ration de cookie. J'ai d\u00e9ja pu travailler lors d'un poc sur la meilleure facon de prendres des screenshots de la F1TV : Avoir une page chrome ouverte avec le feed en plein \u00e9cran et un programme qui prend des captures d'\u00e9crans. Avoir une cam\u00e9ra qui prend en photo l'\u00e9cran au cas ou chrome et Firefox emp\u00eachent la prise de captures d'\u00e9crans. R\u00e9cup\u00e8rer directement le feed en faisant du reverse engeneering de la plateforme. Simuler un chrome en background qui prenne des screenshot sans qu'on aie \u00e0 le voir. Dans toutes ces options, je dirais que la pire \u00e9tait celle de la cam\u00e9ra qui filme l'\u00e9cran, mais \u00e0 l'\u00e9poque c'\u00e9tait encore envisageable comme solution de dernier recours. Le soucis de cette solution c'est l'horreur que serait la partie OCR avec une image de tr\u00e8s mauvaise qualit\u00e9. Une autre option qui m'aurait vraiment emb\u00eat\u00e9 aurait \u00e9t\u00e9 de devoir garder une page de Chrome ou Firefox ouverte quelque part sur un \u00e9cran pour que le programme puisse prendres des captures d'\u00e9crans. C'est de loin l'option la plus simple et la plus logique mais elle poss\u00e8de pour moi de tr\u00e8s gros points noirs : On ne peut pas certifier l'int\u00e9grit\u00e9 des donn\u00e9es car l'utilisateur a le contr\u00f4le total sur le feed. Il peut mettre pause, avancer, reculer, tout casser sans faire expr\u00e8s en ouvrant autre chose sur son ordi qui se mette pile devant. Bref c'est un peu bancale. Et surtout on bloque une partie non significative de l'\u00e9cran de l'utilisateur avec des infos redondantes. Et je peux vous dire que quand je commente la F1 j'ai besoin de beaucoup d'informations et que chaque centim\u00e8tre d'\u00e9cran est crucia\u00e9 ! Alors avoir un \u00e9cran complet bloqu\u00e9 est juste un point bloquant qui m'emp\u00eacherait d'utiliser l'app aussi bonne soit-elle dans ses pr\u00e9dictions. Mais bon si aucune autre methode ne fonctionne ce qui est bien c'est que celle la est plut\u00f4t simple \u00e0 mettre en place. Ensuite reverse engeneer le feed serait l'option la plus classe, cependant c'est la plus complexe et la plus bancale au niveau l\u00e9gal haha. L'id\u00e9e serait de r\u00e9cup\u00e8rer le lien vers le broadcast g\u00e9n\u00e9ral et de comprendre comment il fonctionne pour le d\u00e9coder nous m\u00eame pendant un Grand Prix. Seuls soucis : Il n'est pas possible de faire des tests en dehors des periodes de Grand Prix (Et je rappelle que c'est des p\u00e9riodes ou je travaille en plus) Difficile de faire un syst\u00e8me qui marche pareil pour les rediffusions et les lives. (En effet les liens des rediff sont beaucoup plus simple \u00e0 r\u00e9cup\u00e8rer mais ne fonctionnent pas du tout pareil et pour tester l'app il est essentiel de pouvoir s'entrainer sur des anciens Grand Prix) Dernier GROS soucis, je ne sais tout simplement pas faire ca lol. Je ne sais pas comment faire. Peut-\u00eatre que avec des profs qui m'aident et chat gpt ainsi qu'internet je pourrais potentiellement n\u00e9gocier un truc mais c'est hautement improbable et cela serait une perte de temps folle si je n'y arrive pas. Derni\u00e8re option que je trouve la plus s\u00e9duisante. Simuler une instance de Chrome ou de Firefox (Le soucis avec chrome c'est qu'il impl\u00e9mente l'utilisation de DRM dans les vid\u00e9os qui fait qu'il est tr\u00e8s difficile de passer outre la s\u00e9curit\u00e9 avec un bot) pour ensuite prendre des captures d'\u00e9crans automatiquement. Cette solutions offre pleins d'avantages : Pas de place prise sur l'\u00e9cran L'int\u00e9grit\u00e9 des donn\u00e9es est assur\u00e9e car c'est le programme qui d\u00e9cide d'ou partir et de si il met pause ou non C'est une option complexe mais beaucoup moins que le reverse engeneering Elle permet de ne demander presque aucun input de la part de l'utilisateur. Mais elle pose quelques probl\u00e9matiques : Comment se connecter automatiquement sans \u00eatre detect\u00e9 par un Bot et sans demander \u00e0 l'utilisateur ses identifiants (Pour des raisons \u00e9videntes qui sont : QUI VA METTRE SES IDENTIFIANTS SUR UNE VIEILLE APP COMME LA MIENNE??) Comment faire en sorte que le programme prenne les meilleures captures dans la meilleure qualit\u00e9 et en plein \u00e9cran. Mais j'ai d\u00e9cid\u00e9 de partir sur cette option. Pour ce faire j'utilise Selenium. J'ai pu tester Puppetteer Sharp et m\u00eame si dans un premier temps j'ai pu avancer asez vite, malheureusement il y a des bugs qui rendent son utilisation impossible dans notre contexte. J'ai donc d\u00e9cid\u00e9 de tout faire en utilisant un portage de Selenium dans mon programme. Voici un exemple de code qui va ouvrir FireFox et qui va lancer un RickRoll var service = FirefoxDriverService.CreateDefaultService(AppDomain.CurrentDomain.BaseDirectory+@\"geckodriver-v0.27.0-win32\\geckodriver.exe\"); service.Host = \"127.0.0.1\"; service.Port = 5555; FirefoxOptions options = new FirefoxOptions(); options.AddArgument(\"--disable-headless\"); var driver = new FirefoxDriver(service,options); driver.Navigate().GoToUrl(\"https://www.youtube.com/watch?v=dQw4w9WgXcQ&autoplay=1&mute=1\"); Dans cet exemple on d\u00e9sactive le \"Headless\" pour qu'on puisse voir ce que fait l'app car sinon tout est invisible. Alors dans les faits la vid\u00e9o youtube ne se lance pas du tout car il y a des pubs et des prompts de cookies que l'on doit accepter etc... ce qui montre les diff\u00e9rents challenges que l'on va devoir surmonter pour vraiment faire ce que l'on veut. Mais un petit d\u00e9tail extr\u00eamement important, la F1TV est un programme payant un peu comme netflix. Ce qui veut dire que pour acc\u00e8der au contenu il faut \u00eatre connect\u00e9. Sauf que une instance de firefox cr\u00e9\u00e9 par Selenium est comme une page de naviguation priv\u00e9e, ce qui veut dire que si on va sur la page de la F1TV on est pas connect\u00e9s. Je pourrais tout \u00e0 fait demander \u00e0 l'utilisateur de me donner ses identifiants pour que j'aille ensuite automatiquement me connecter sauf que cela pose deux soucis: Personne ne voudra mettre ses identifiants sur mon programme La page de login de la F1TV a \u00e9t\u00e9 prot\u00e8g\u00e9e avec la meilleure technologie de detection de bots que je connaisse. Presque aucun site n'arrive \u00e0 me detecter sauf eux ! Donc c'est tout simplement impossible d'utiliser cette technique. Ensuite je me suis rappel\u00e9 que ce que la page stocke pour me permettre de rester connect\u00e9 ce sont des cookies. Et si je mets le bon cookies dans Selenium alors je serai connect\u00e9. Dans un premier temps je voulais faire un syst\u00e8me ou l'utilisateur irait prendre dans son chrome son cookie et le copie colle dans mon programme mais c'est immonde. C'est alors que vient la partie r\u00e9cup\u00e8ration de cookies ! Tous les cookies de chrome sont stock\u00e9s dans une base de donn\u00e9es SQLITE. On pourrait se dire Banco il suffit d'aller dedans et de retrouver tous les cookies et se connecter. Sauf que, pas b\u00eates, les \u00e9quipes de chrome ont d\u00e9cid\u00e9 que c'\u00e9tait une bonne id\u00e9e d'encoder les cookies pour que tout le monde ne puisse pas venir y mettre son nez... En effet les cookies peuvent contenir des informations importantes. Cela fait que pour utiliser ces cookies il faut pouvoir les d\u00e9coder. Mon hypoth\u00e8se a \u00e9t\u00e9 que si ces cookies peuvent \u00eatre lus par Chrome m\u00eame hors connexion, c'est que la cl\u00e9 de d\u00e9codage existe sur l'appareil et qu'il suffit de la trouver. ET C'EST LE CAS! Apr\u00e8s pas mal de recherches j'ai pu voir que la cl\u00e9 de d\u00e9codage existe bel et bien et qu'il suffit de la d\u00e9coder en utilisant la librairie DPAPI pour la lire. Avec cette cl\u00e9 on peut ensuite d\u00e9coder les cookies et leurs valeurs ce qui veut dire qu'il est th\u00e9oriquement possible d'automatiser le processus sans que l'utilisateur n'aie rien \u00e0 faire. J'ai d\u00e9cid\u00e9 de faire la partie r\u00e9cup\u00e8ration en python pour deux raison : Je n'arrivais pas \u00e0 trouver une bonne impl\u00e9mentation de DPAPI en C# qui me permettait de d\u00e9coder la cl\u00e9. Il existe beaucoup plus de documentation en Python pour ce qui est de la cryptographie et donc si Chrome change de fonctionnement il sera beaucoup plus simple de changer cette partie en particulier sans avoir \u00e0 recompiler le code C#. J'ai donc avec l'aide d'internet et de ChatGPT cr\u00e9\u00e9 ce script : def get_master_key(): with open( os.getenv(\"localappdata\") + \"\\\\Google\\\\Chrome\\\\User Data\\\\Local State\", \"r\" ) as f: local_state = f.read() local_state = json.loads(local_state) master_key = base64.b64decode(local_state[\"os_crypt\"][\"encrypted_key\"]) master_key = master_key[5:] # removing DPAPI master_key = win32crypt.CryptUnprotectData(master_key, None, None, None, 0)[1] print(\"MASTER KEY :\") print(master_key) print(len(master_key)) return master_key def decrypt_payload(cipher, payload): return cipher.decrypt(payload) def generate_cipher(aes_key, iv): return AES.new(aes_key, AES.MODE_GCM, iv) def decrypt_password(buff, master_key): try: iv = buff[3:15] payload = buff[15:] cipher = generate_cipher(master_key, iv) decrypted_pass = decrypt_payload(cipher, payload) decrypted_pass = decrypted_pass[:-16].decode() # remove suffix bytes return decrypted_pass except Exception: # print(\"Probably saved password from Chrome version older than v80\\n\") # print(str(e)) return \"Chrome < 80\" master_key = get_master_key() cookies_path = Path( os.getenv(\"localappdata\") + \"\\\\Google\\\\Chrome\\\\User Data\\\\Default\\\\Network\\\\Cookies\" ) if not cookies_path.exists(): raise ValueError(\"Cookies file not found\") with sqlite3.connect(cookies_path) as connection: connection.row_factory = sqlite3.Row cursor = connection.cursor() cursor.execute(\"SELECT * FROM cookies\") with open('cookies.csv', 'a', newline='') as csvfile: fieldnames = ['host_key', 'name', 'value', 'path', 'expires_utc', 'is_secure', 'is_httponly'] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) if csvfile.tell() == 0: writer.writeheader() for row in cursor.fetchall(): decrypted_value = decrypt_password(row[\"encrypted_value\"], master_key) writer.writerow({ 'host_key': row[\"host_key\"], 'name': row[\"name\"], 'value': decrypted_value, 'path': row[\"path\"], 'expires_utc': row[\"expires_utc\"], 'is_secure': row[\"is_secure\"], 'is_httponly': row[\"is_httponly\"] }) print(\"Finished CSV\") Ce programme va faire tout ce que j'ai expliqu\u00e9 et va ensuite stocker les r\u00e9sultats dans un CSV pour qu'il soit facile d'y acc\u00e8der depuis le C#. Alors oui cela pose certaines questions de s\u00e9curit\u00e9. Car en effet je prend tous les cookies, les d\u00e9code et les stocke. Ce qui veut dire que je pourrais tout \u00e0 fait envoyer ces donn\u00e9es quelque part, par exemple un compte Netflix, et me rincer. Si je devais rendre le projet ouvert au public je pense qu'il faudra que cela soit mentionn\u00e9 clairement et que le projet soit open source pour que les utilisateurs puissent verifier que je ne fais pas ca. Maintenant de l'autre c\u00f4t\u00e9 j'ai juste \u00e0 lire le CSV et le tour est jou\u00e9 ! (Trouver cette solution m'a pris une semaine de vacances \u00e0 l'\u00e9poque) Bon j'ai r\u00e9ussi \u00e0 faire le programme se connecter et naviguer etc.. Par contre quelque chose que j'ai voulu ajouter et qui m'a pris pas mal de temps c'est de faire en sorte de pouvoir selectionner la qualit\u00e9. Pour changer la qualit\u00e9 du feed il faut cliquer sur settings et ensuite prendre le menu deroulant et selectioner 1080p. Le soucis c'est le que la value du select est jamais la m\u00eame. Elle commence toujours pas \"1080_\" mais ensuite ca peut \u00eatre \"1080_45930285\" ou \"1080_56801\" la suite est apparemment random. J'ai donc du utiliser ce code pour le selectioner quand m\u00eame : IWebElement settingsButton = driver.FindElement(By.ClassName(\"bmpui-ui-settingstogglebutton\")); settingsButton.Click(); IWebElement selectElement = driver.FindElement(By.ClassName(\"bmpui-ui-videoqualityselectbox\")); SelectElement select = new SelectElement(selectElement); IWebElement selectOption = selectElement.FindElement(By.CssSelector(\"option[value^='1080_']\")); selectOption.Click(); Sauf que pour que cela marche je dois avant cliquer sur le bouton des settings le probl\u00e8me c'est qu'il est invisible alors on doit le faire apparaitre. J'ai tent\u00e9 de le faire aparaitre en bougeant la souris, en cliquant \u00e0 un endroit pr\u00e9cis, impossible de le faire marcher correctement. Puis j'ai eu l'id\u00e9e de mettre pause en envoyant un appui sur la touche Espace et ca a permit de d\u00e9couvrir le bouton et permettre qu'on clique dessus. Ca peut paraitre tout b\u00eate mais rien que ca, ca m'a pris un temps consid\u00e9rable. Bon pour ce qui est du timecode de la vid\u00e9o. Je pense qu'il serait trop complexe de faire en sorte que selenium change le slider de progression de la vid\u00e9o. Alors j'ai fait quelques tests et apparemment, si on quitte la F1TV sur un timecode de la vid\u00e9o que on donne au programme, comme il r\u00e9cup\u00e8re tous les cookies de la F1TV il commencera de la. Donc si on veut utiliser le programme avec des Grand Prix ayant d\u00e9ja eu lieu, on peut le faire, seulement il faudra juste au pr\u00e9alable avoir choisit le bon timecode dans le page de la F1TV avant de le lancer. Ce qui est int\u00e9ressant c'est que la page de la F1TV ressemble \u00e0 ca au d\u00e9part : \"Empty F1TV\" Je pense qu'une bonne id\u00e9e serait de dire au programme que c'est la grille de d\u00e9part et ensuite d\u00e8s qu'il d\u00e9tecte un secteur il sait que la course a commenc\u00e9.","title":"Vacances"},{"location":"jdb.html#lundi-24-avril-2023","text":"Aujourd'hui c'est jour de documentation. J'ai pas mal travaill\u00e9 pendant les vacances mais je n'ai pas encore pu faire de vraie documentation correcte du fonctionnement. Du coup je vais m'en charger aujourd'hui et peut-\u00eatre un peu demain. Ok normalement je ne devrais faire que de la documentation mais je ne peux pas passer \u00e0 cot\u00e9 de ca... Le probl\u00e8me que j'ai avec les pneus ou parfois il d\u00e9tecte un H au lieu d'un '11' et ce genre de choses c'est \u00e0 cause de ma methode \"RemoveBG\" Qui va retirer tous les pixels plus sombres que le background. Sauf que cela va aussi retirer des pixels dans le chiffre lui m\u00eame et qui va donc defigurer les 11 : \"diformed 11\" \"diformed 11\" J'ai r\u00e9ussi \u00e0 les changer en : \"less diformed\" \"less diformed\" Mais au final cela n'a pas augment\u00e9 la pr\u00e9cision de la reconnaissance. Je pense que je vais donc devoir encore changer. Je pense que une bonne facon de trouver serait dabord de trouver la couleur du pneu. Et si il n'y a pas assez de couleur alors c'est que le pneu contient une lettre. Le but est d'arr\u00eater de chercher des lettres ou des chiffres. Comme ca les 11 arr\u00eateront d'\u00eatre pris pour des 'H' En fait on peut faire encore plus simple que ca. On peut simplement regarder la couleur dominante et determiner le pneu. En effet m\u00eame si il y a une lettre sur fond noir pour d\u00e9crire le pneu, mon methode de r\u00e9cup\u00e8ration de la couleur dominante ommet les pixels trop noirs alors il est quand m\u00eame possible de determiner le type de pneus. Et tout simplement si il n'arrive pas \u00e0 lire le chiffre c'est que c'est une lettre et que donc on est \u00e0 0 tours. Cela marche plut\u00f4t bien et cela simplifie pas mal le processing. Voila, la je vais me remettre \u00e0 la documentation sinon je vais encore prendre du retard.","title":"Lundi 24 Avril 2023"},{"location":"jdb.html#mardi-25-avril-2023","text":"Encore une fois j'ai pris du temps de doc pour changer des choses sur la partie OCR. Mais en m\u00eame temps en documentant je vois des choses que j'ai soit mal fait soit que je pourrais faire mieux en changeant tr\u00e8s peu de choses. J'\u00e9sp\u00e8re que les changement que j'ai fait vont aider au moins \u00e0 la coh\u00e9rence du code et un peu pour les performances. Il semble que dans les conditions que j'ai test\u00e9 le nombre de tour soit plut\u00f4t fiable mais je pense que je devrai faire un peu de travail en aval dans la r\u00e9cup\u00e9ration de ces donn\u00e9es car je sens que cela va poser probl\u00e8me quelques fois. Je pense que en utilisant bien l'historique on peut potentiellement se passer de l'utilisation de ce chiffre pas toujours compl\u00eatement fiable. Mais sinon aujourd'hui c'est encore une fois un gros jour de doc. J'essaie d'expliquer les diff\u00e9rents proc\u00e9d\u00e9s avant de les oublier. J'essaie aussi de donner un maximum d'exemples sous formes de photos interm\u00e9diaires mais ca me prend pas mal de temps car il faut que j'ajoute un peu partout dans le code des lignes pour sortir des images interm\u00e9diaires. En plus de la documentation je me suis aussi beaucoup occup\u00e9 de nettoyer mon code et je suis assez content par ce que m\u00eame en ayant du rajouter des couches de complexit\u00e9 pour mieux reconnaitres les temps au tour j'arrive \u00e0 un temps de processing parfois en dessous des 2 secondes ce que je trouve honorable. Quand j'aurai finit de nettoyer tous mes fichiers je ferai une release sur gitea et ce sera la version que j'utiliserai quand je voudrai faire un merge avec les autres parties du projet. J'ai beaucoup beaucoup boss\u00e9 aujourd'hui et je sui bien mort. Faire autant de documentation et de nettoyage de code c'est pas forc\u00e9ment bon pour le cerveau je crois. J'ai besoin d'une sieste. Demain je pense que je vais commencer \u00e0 avancer sur la partie r\u00e9cup\u00e8ration des images. Je sais que la je fais un peu passer les tests \u00e0 la trappe mais d\u00e9ja j'en ai fait tout le long du d\u00e9veloppement de OCR_DECODE et il faut vraiment que j'avance, quitte \u00e0 revenir dessus quand j'aurai merge les deux projets ensemble.","title":"Mardi 25 Avril 2023"},{"location":"jdb.html#26-avril-2023","text":"Aujourd'hui je vais devoir m'occuper de la partie r\u00e9cup\u00e9ration des images. J'ai d\u00e9ja eu l'occasion d'avancer sur ce projet pendant mopn poc et mes vacances. Donc la le but ca va \u00eatre de voir ce qui manque comme v\u00e9ritables features et ensuite je vais pouvoir m'occuper de la vue et de son int\u00e9gration avec le d\u00e9codage. Ok donc maintenant que j'au un programme qui arrive \u00e0 prendre des images depuis la F1TV correctement et en bonne r\u00e9solution. Je pense qu'il est temps de passer \u00e0 l'impl\u00e9mentation de la Forme que ca va prendre. C'est important de se poser au moins cinq minutes la question de comment je pr\u00e9vois de faire car m\u00eame si ca n'est pas la version finale, cette derni\u00e8re prendra tr\u00e8s fort inspiration du desing que je vais faire. Dans cette form j'aurais besoin de : Pouvoir selectionner un Grand Prix en ins\u00e8rant l'URL du feed. Pouvoir lancer la calibration si besoin Indiquer le titre et la date du Grand Prix Indiquer si le Grand Prix vient de commencer ou si il y a d\u00e9ja un certain nombre de tours lanc\u00e9s. Et c'est \u00e0 peu pr\u00e8s tout en fait... J'ai tellement pouss\u00e9 pour avoir un programme qui fait tout tout seul que il ne me faut pas grand chose de plus. Je pense que ce qui serait pas mal ca serait du coup d'utiliser ce temps pour bien impl\u00e9menter la calibration qui elle aura besoin d'une UI un peu plus bal\u00e8ze. On pourrait m\u00eame imaginer que la calibration fasse partie int\u00e9grante des settings... Ca serait peut-\u00eatre bien que quand l'application se lance on se retrouve sur la page principale d'affichage de donn\u00e9es et qu'on puisse simplement cliquer sur la page options qui contient la page calibration et qui permet de rentrer les infos du Grand Prix. Je pense que je vais faire ca. Voici l'interface que j'ai d\u00e9velopp\u00e9e pour regrouper tout ca : \"Screen\" La police le style le placement et les couleurs ne sont pas d\u00e9finitfs, cependant je pense que c'est un bon d\u00e9but. Le but maintenant va \u00eatre de permettre de faire fonctionner la calibration et la r\u00e9cup\u00e8ration d'images. Si j'arrive \u00e0 faire fonctionner ces deux choses sur un m\u00eame projet avant la fin de la semaine cela serait super ! Bon J'ai pu avancer sur l'int\u00e8gration de Selenium mais cela prend un peu de temps car je veux impl\u00e9metner un moyen de pouvoir prendre une Screenshot \u00e0 nimporte quel moment et pas juste en boucle. Demain je finis de faire fonctionner ca et ensuite je commence le cablage du reste.","title":"26 Avril 2023"},{"location":"jdb.html#jeudi-27-avril-2023","text":"C'est assez dur de faire l'importation car il y a des petites diff\u00e9rences qui obligent \u00e0 presque tout r\u00e9\u00e9crire. En fait le programme de calibration avait d\u00e9ja impl\u00e9ment\u00e9 la fonction de Windows et de Zones mais il fonctionnait juste assez diff\u00e9remment pour qu'il faille tout refaire. La je suis en train de perdre \u00e9norm\u00e9ment de temps \u00e0 cause d'un soucis de coordonn\u00e9es. J'ai repris le code de la calibration pour detecter ou l'utilisateur a cliqu\u00e9 pour cr\u00e9er les zones. Cependant, je n'arrive pas \u00e0 le faire fonctionner correctement. La zone est tout le temps d\u00e9cal\u00e9e en haut et en bas mais pas de la m\u00eame facon. En haut, la valeur Y est trop grande alors que en bas la valeur Y est trop petite... Je ne comprends pas bien pourquoi. Si c'\u00e9tait un simple d\u00e9calage cela ne serait pas compliqu\u00e9 \u00e0 g\u00e8rer mais la... J'ai un soucis \u00e9galement avec la r\u00e9solution des screenshots que je r\u00e9cup\u00e8re en full Headless. Voici un exemple de r\u00e9solution que j'arrive \u00e0 r\u00e9cup\u00e8rer sans le headless : \"High Res\" \"Low Res\" Il y a clairement un soucis et le probl\u00e8me c'est que avec une r\u00e9solution pareille, impossible de faire une reconnaissance correcte. BON J?EN PEUX PLUS LA. Ca fait des heures que je bosse sur ce probl\u00e8me d\u00e9bile et impossible de trouver une solution. J'ai essay\u00e9 cinq facons de forcer le browser headless a prendre une plus haute r\u00e9solution aucune ne fonctionne je ne comprends pas. A chaque fois que je me retrouve avec une r\u00e9solution de 1366 x 768 Ou une variante de basse r\u00e9solution du style. J'en peux plus je ne trouve aucune r\u00e9ponse sur internet ni m\u00eame avec chatGPT. Super... La seule chose que j'ai pu faire qui change quelque chose fait que les images font maintenant du 926x517... j'ai un peu envie de commentre un crime de guerre au plus vite.","title":"Jeudi 27 Avril 2023"},{"location":"jdb.html#vendredi-28-avril-2023","text":"Une des solutions que je n'ai pas encore essay\u00e9 est de changer ma version de GeckoDriver. Sauf que ca m'oblige \u00e0 changer les versions de mes libraries ce qui est tr\u00e8s p\u00e9nible, je vais continuer le debugging dans le projet Selenium_clean. Il faut savoir que la librairie de Selenium que j'utilise est bloqu\u00e9e en 0.27 ce qui fait que je ne peux utiliser qu'une version obsol\u00e8te du Gecko Driver. J'ai tent\u00e9 de changer vers une version en 64 bits du GeckoDriver 0.27 mais pareil, je me retrouve toujours avec des images de M. J'essaie toutes les solutions que je trouve sur internet aucune ne convient c'est infernal. J'essaie de changer la r\u00e9solution DPI, j'essaie de changer les param\u00eatres par d\u00e9faut des player de Firefox, j'essaie de changer la r\u00e9solution pendant et au d\u00e9but de l'execution IMPOSSIBLE DE FAIRE MARCHER CETTE MERDE C'EST PAS POSSIBLE !!! J'ai essay\u00e9 avec chrome mais je ne peux pas l'utiliser car les DRM m'emp\u00eacheront de prendre des screenshot du flux vid\u00e9o. J'ai essay\u00e9 de faire tourner avec edge mais edge ne peut pas tourner en headless. JE VAIS DEVENIR FOUF FPWQOVMQEKOVNVIBDBJDAIVOBI. ET MAINTENANT JE N'ARRIVE PLUS A FAIRE DE PROJET AVEC SELENIUM VOIWQNV(UEWQBVU)WEQN=OEJNIVIUWVBWUEV ON CHERCHE A ME FAIRE PETER UN PLOMB C'EST PAS POSSIBLE GIWEGUWEQN VOICI UN EXEMPLE DU CODE QUE JE DEMANDE A UN NOUVEAU PROJET AVEC EXACTEMENT LES MEMES LIBRARIES INSTALLEES : // Create a new FirefoxDriver instance IWebDriver driver = new FirefoxDriver(); // Navigate to the specified URL driver.Navigate().GoToUrl(\"https://www.example.com\"); // Do something with the driver (e.g., find elements or take screenshots) // Quit the driver driver.Quit(); Je ne demande que ca ET MEME CA CA NE VEUT PAS FONCTIONNER VOIWENB)IWUQENV Oui je suis un peu \u00e9nerv\u00e9 ca se voit? A bon? Et maintenant NUGGET ne fonctionne plus... j'en peux plus la. Je ne peux plus t\u00e9l\u00e9charger de librairie sur aucun de mes projets... J'ai tent\u00e9 de supprimer le fichier de config et red\u00e9marrer Visual Studio mais cela ne fait rien. J'ai aussi tent\u00e9 de faire un 'nugget restore' toujours rien. Bon apparemment je ne suis pas le seul qui ne peut pas acc\u00e8der \u00e0 Nuget donc bon c'est pas juste chez moi qu'il y a un soucis. Mais m\u00eame en mettant ma 4G pour me connecter, je n'arrive pas \u00e0 acc\u00e8der \u00e0 certains sites y compris Nuget et je ne peux pas download de librairies... Je ne comprends pas ce qui se passe et du coup je ne peux juste pas bosser... J'ai red\u00e9marr\u00e9 trois fois mon pc et visual studio, j'ai essay\u00e9 de changer mes settings DNS etc... impossible de bosser. Je crois que je n'aurais pas du me reveiller aujourd'hui. Bon je vais tenter d'avancer sur mon poster en attendant que le r\u00e9seau soit en meilleur \u00e9tat.","title":"Vendredi 28 Avril 2023"},{"location":"jdb.html#lundi-1-mai-2023","text":"Bon je bosse depuis chez moi donc j'esp\u00e8re que Nuget va mieux fonctionner. Apr\u00e8s un weekend \u00e0 r\u00e9fl\u00e9chir au sujet de cette resolution je me suis dit deux choses. La seule personne sur internet que j'ai vu avoir le meme soucis avait une r\u00e9solution de 1920x1200 comme moi. Cela veut donc s\u00fbrement dire que le soucis vient de cette r\u00e9solution de laptop comme moi. Si vraiment je n'arrive pas dans un premier temps \u00e0 faire fonctionner le Headless correctement, je peux toujours laisser la page de c\u00f4t\u00e9 et m'occuper du reste du programme. Certes ca serait vraiment infernal d'avoir \u00e0 garder une page chrome ouvert en tous temps et en plus elle doit \u00eatre en plein \u00e9cran mais bon... Si il n'y a vraiment pas d'autres solutions malheureusement je serai bien oblig\u00e9. BON ! JE N'ARRIVE MEME PLUS A FAIRE UN PROJET QUI UTILISE SELENIUM ET QUI MARCHE JE VAIS FAIRE BR\u00dbLER GENEVE. C'est pas possible serieux, je ne comprends pas j'essaie tout ce que je trouve et impossible de juste lancer firefox c'est du grand nimporte quoi. Je prend les m\u00eame putain de librairies que sur les autres projets les m\u00eames versions, je prend le m\u00eame exact code. Sur le nouveau projet impossible de le faire fonctionner. Je commence \u00e0 croire que on essaie de me faire p\u00eater un cable. Du coup dans un \u00e9lan de d\u00e9sespoir je vais tenter de passer sur une autre librairie qui avec un peu de chance marche et en plus me permettrais de prendre des foutues screenshot dans le bon format. Les deux seules librairies qui pourraient potentiellement faire l'affaire sont les librairies : PhantomJS CefSharp Je vais les tester et simplement prier pour qu'elles fonctionnent et que je puisse faire ce que je veux avec. Alors pour le moment avec CEFSharp j'arrive \u00e0 lancer une instance de chrome et prendre une screenshot avec ce code : CefSettings settings = new CefSettings(); settings.CachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), \"CefSharp\\\\Cache\"); // Set cache path settings.LogSeverity = LogSeverity.Disable; // Disable logging Cef.Initialize(settings); // Initialize CEF using (var browser = new ChromiumWebBrowser(\"www.google.com\", new BrowserSettings())) // Launch Chromium in off-screen mode { browser.Load(\"https://www.example.com\"); // Navigate to the test URL browser.Size = new Size(1920, 1080); // Set the browser size to 1920x1080 browser.ScreenshotAsync().ContinueWith(task => { var bitmap = task.Result; bitmap.Save(\"screenshot.png\", System.Drawing.Imaging.ImageFormat.Png); // Take a screenshot and save it as a PNG file }).Wait(); } Cef.Shutdown(); // Shutdown CEF Avec ca il faut ces using : using System; using System.Drawing; using System.IO; using CefSharp; using CefSharp.OffScreen; C'est assez prometteur m\u00eame si il faut encore beaucoup pour remplacer selenium. Ah bah lol en fait non on peut pas utiliser cette librarie pour faire tourner firefox... J'EN AI MARRE J'AVAIS CHERCHE PRECISEMENT UNE LIB QUI MARCHE AVEC FIREFOX Et phantomJS non plus ne fonctionne pas avec firefox... J'en ai marre. Donc je vais plut\u00f4t partir sur la librairie GeckoFX qui semble pouvoir contr\u00f4ler une instance de firefox. Mais j'avais justement pris un putain de projet C# et pas JS pour ne pas me taper ces probl\u00e8mes de librairies... Et si cette option ne fonctionne pas mon dernier espoir sera de directement int\u00e9ragir avec le geckodriver.exe et la ca risque de pas \u00eatre dr\u00f4le. JE NE COMPRENDS RIEN !!!!! Ca n'a aucun sens la doc est inexistante le seul lien qui pourrait amener sur une doc envoie sur la page principale de bitbucket. Tous les exemples de code que je trouve ne fonctionnent pas. Je n'arrive \u00e0 rien je commence \u00e0 devenir fou. Tout ce travail pour rien c'est pas possible. M\u00eame en essayant directement d'int\u00e9ragir avec le process geckodriver.exe je ne peux pas arriver \u00e0 mes fins. J'arrive \u00e0 lancer le service et tout, mais je n'arrive pas \u00e0 vraiment contr\u00f4ler ce qu'il se passe donc impossible de venir prendre des screenshot. Je ne sais tout simplement pas quoi faire ... Je suis bloqu\u00e9. Je me suis cass\u00e9 la t\u00eate \u00e0 faire un truc qui marchait bien avec selenium et tout. Mais maintenant plus rien ne fonctionne du jour au lendemain et il n'y a simplement aucune alternative. Je vais essayer de changer directement le projet Selenium_Clean mais bon ca va pas \u00eatre dr\u00f4le. Ok alors j'ai tout repris depuis le d\u00e9but et je crois que j'ai enfin une solution. Pour la trouver j'ai re-essay\u00e9 toutes les techniques que j'avais tent\u00e9 avant mais dans l'ordre et en les isolant \u00e0 chaque fois. Cela inclus : Tenter de changer la densit\u00e9 de pixels. En effet je me suis dit que comme la r\u00e9solution \u00e9tait plus basse le soucis \u00e9tait que le virtual screen avait simplement une DPI r\u00e9duite. profile.SetPreference(\"layout.css.devPixelsPerPx\", \"2.0\"); J'ai aussi tent\u00e9 de r\u00e9duire \u00e0 un seule le nombre de process de Firefox. J'ai pu lire sur internet que parfois cela pouvait influer sur les performances du renderer. profile.SetPreference(\"dom.ipc.processCount\", 1); Ensuite j'ai tent\u00e9 tout b\u00eatement de rajouter dans la liste des arguments la taille voulue de l'\u00e9cran. options.AddArgument(\"--window-size=1920,1080\"); Mais comme cela ne foncionnait pas, je me suis rabattu sur un script JS pour tenter de forcer la fen\u00eatre \u00e0 \u00eatre plus grande. js.ExecuteScript(\"window.resizeTo(1920, 1080);\"); Comme cela n'a pas march\u00e9 j'ai pu lire que cela pouvait \u00eatre la taille int\u00e9rieure qui devait \u00eatre chang\u00e9e js.ExecuteScript(\"window.innerWidth = 1920; window.innerHeight = 1080;\"); Encore une fois sans succ\u00e8s. J'ai ensuite tent\u00e9 d'utiliser trois autres versions du GeckoDriver, 0.27,0.26,0.25 et aucune ne m'aidait. Mais en fait la seule chose qui a chang\u00e9 quoi que ce soit \u00e9tait la technique suivante : Changer la window size en utilisant : options.AddArgument(\"--width=1920\"); options.AddArgument(\"--height=1200\"); Ca ne marchait pas car j'utilisais une autre methode pour resize en m\u00eame temps, qui elle ne marchait pas mais qui emp\u00eachait celle la de marcher. Ensuite le soucis que j'avais c'est que en mettant 1920-1080 je me retrouvais avec 1920-998 ou un truc du genre ce qui n'\u00e9tait pas normal alors je me disais que cette technique ne marchait pas non plus et je l'ai pass\u00e9e. Alors tout n'est pas encore gagn\u00e9, il faut que j'arrive \u00e0 impl\u00e9menter ca dans un plus gros projet et que la vid\u00e9o puisse \u00eatre prise seule. Demain je m'occupe de ca.","title":"Lundi 1 Mai 2023"},{"location":"jdb.html#mardi-2-mai-2023","text":"Bon aujourd'hui je change le programme principal. Le soucis que j'ai c'est que en ajoutant ce syst\u00e8me de resize, maintenant la page fait 100x100 et est grise. Il doit y avoir une technique que j'ai oubli\u00e9 de retirer ou un comportement un peu bizarrre. Bon clairement je ne sais pas QUI DECIDE DE ME POURRIR LA VIE mais il est fort. J'ai t\u00e9l\u00e9charger EXACTEMENT les m\u00eames librairies que sur mon autre projet et j'utilise l'EXACT m\u00eame geckodriver.exe mais dans le projet principal impossible de lui faire chier une image m\u00eame avec l'EXACT m\u00eame code. POURQUOI VOUS ME FAITES CA????= La je ne comprend vraiment pas ce qui peut se passer pour que rien ne fonctionne alors que tout est pareil. JE VIENS DE TOUT VERIFIER TOUT EST PAREIL JE NE COMPRENDS PAS. Bon apr\u00e8s avoir supprim\u00e9 l'int\u00e9gralit\u00e9 de ma classe Emulator cela semble marcher un peu mieux. Je ne vais pas m'\u00e9tendre sur la castrophe niveau temps que cela repr\u00e9sente. Si au moins j'arrive \u00e0 faire fonctionner quelque chose je suis content. Maintenant j'ai un soucis un peu sp\u00e9cial. Depuis que j'ai chang\u00e9 la r\u00e9solution, il semble que le programme aie du mal \u00e0 cliquer sur l'icone de settings. En prenant des screenshots du moment ou l'erreur apparait, j'ai pu me rendre compte que en fait le stream est toujours en train de charger et c'est pour ca que on arrive pas \u00e0 trouver le bouton : \"ERROR 105\" \"ERROR 105\" Je pense que je n'ai le soucis que maintenant car le flux en 1080p se lance moins vite. Je vais essayer de voir si je peux detecter un \u00e9l\u00e9ment d'HTML qui correspond au loading comme ca je peux attendre qu'il disparaisse. Sinon je peux aussi juste essayer de trouver le bouton en boucle pendant une dixaine de secondes. Bon la j'essaie pendant genre plus de 50 secondes et ca ne marche toujours pas. Il semblerait que au final le probl\u00e8me vienne du GP d'azerbidjan. En effet, quand je teste un autre Grand Prix tout va bien. ET MERDE ! J'ai r\u00e9ussi \u00e0 avoir des images en 1080P mais d\u00e9s que je passe l'image en plein \u00e9cran c'est de nouveau du 1366X768 Avant de mettre en plein \u00e9cran: \"Before fullscreen\" Apr\u00e8s: \"After fullscreen\" On peut voir sur l'image que l'option 1080P est effectivement bien selectionn\u00e9e mais il doit y avoir un param\u00e8tre de Firefox qui s'occupe de la r\u00e9solution d'un player vid\u00e9o. Il va juste falloir trouver ce param\u00eatre... J'ai essay\u00e9 d'utiliser : Driver.Manage().Window.Size = new System.Drawing.Size(windowWidth, windowHeight); Sans succ\u00e8s. options.AddArgument(\"--start-maximized\"); Pareil Driver.Manage().Window.Maximize(); Toujours rien profile.SetPreference(\"full-screen-api.ignore-widgets\", true); Nada profile.SetPreference(\"media.hardware-video-decoding.enabled\", true); Toujours pas J'ai vraiment cru que j'avais trouv\u00e9 la solution en trouvant cette commande profile.SetPreference(\"full-screen-api.enabled\", true); Mais non toujours pas... Je commence \u00e0 perdre patience. C'EST BON. Apr\u00e8s litt\u00e9rallement 3h de debugging avec M.Bonvin (Que je remercie IMMENSEMENT) on a r\u00e9ussi \u00e0 trouver au fin fond d'un thread github que la valeur \u00e9tait hard cod\u00e9e dans les variables d'environnement et que donc quoi que je fasse je n'aurais pas pu le changer. En fait la seul moyen de tout r\u00e8gler a \u00e9t\u00e9 de changer les variables d'environnement de ma machine: MOZ_HEADLESS_WIDTH et MOZ_HEADLESS_HEIGHT . Et ce qu'il y a de bien c'est que maintenant je peux mettre de la 4K et cela permet de faire un meilleur upscaling.","title":"Mardi 2 Mai 2023"},{"location":"jdb.html#recrutement-payerne-mai-2023","text":"J'ai du faire mon recrutement \u00e0 Payerne Mercredi et Jeudi. Si vous \u00eates curieux je peux vous dire que comme il n'y avait presque plus de places cet \u00e9t\u00e9 je ferai Canonnier Lance mines. C'\u00e9tait assez frustrant d'avoir perdu deux jours de travail mais on va faire avec.","title":"Recrutement Payerne Mai 2023"},{"location":"jdb.html#vendredi-5-mai-2023","text":"Bon malgr\u00e9s les courbatures il faut que je me mette au boulot un peu serieusement par ce que sinon ca va \u00eatre compliqu\u00e9 de rattraper mon retard. La derni\u00e8re fois si je me souviens bien j'avais r\u00e9ussi \u00e0 trouver un moyen de prendres des images en bonne r\u00e9solution. Il faut maintenant que je commence \u00e0 faire fonctionner la calibration et ce qui serait bien ca serait que je commence \u00e0 ajouter la partie OCR au projet. Il faut que je me d\u00e9p\u00eache car Lundi je dois m'occuper du Poster. OK j'ai compris le soucis que j'avais quand j'essayais de faire la calibration. J'avais mis l'image en ZOOM ce qui fait que si la hauteur n'\u00e9tait pas la bonne, l'image \u00e9tait recentr\u00e9e ce qui fait que cela faussait totalement les r\u00e9sultats. Quand on fait en sorte que l'image prenne toute la place, les coordonn\u00e9es sont prises correctement. Voici un exemple d'ou en est la partie calibration. \"Exemple settings UI\" Normalement il me suffit d'impl\u00e9menter les windows, et on devrait relativement facilement ajouter les pilotes. Et voila. J'ai pu impl\u00e9menter les windows et les pilotes. Et je peux aussi exporter des presets et les loader. Bon le loading est un peu beugg\u00e9 au niveau de l'affichage mais il semble qu'il fonctionne bien quand je save les images. Lundi je m'occupe du poster etc.. mais je pense que la suite va \u00eatre l'impl\u00e9mentation de l'OCR.","title":"Vendredi 5 Mai 2023"},{"location":"jdb.html#lundi-8-mai-2023","text":"Aujourd'hui c'est journ\u00e9e Poster. Je pense que je ne vais pas finir la journ\u00e9e content car les limitations sont un peu trop pr\u00e9sentes. J'ai fait une version que Garcia pourrait accepter, c'est \u00e0 dire en noir et blanc et avec un tout petit peu plus de d\u00e9tail. \"Poster V3\" Le truc c'est que en blanc je trouve que ca ne marche pas super. Et le concept d'avoir trois parties au projet qui se posent autour d'un circuit c'est peut-\u00eatre pas la meilleure id\u00e9e. Je me suis dit que la bonne id\u00e9e serait peut-\u00eatre de prendre un autre circuit pour qu'il y aie bien trois parties : \"Poster V4\" Clairement ce poster doit faire partie des pires. C'est pas clair et ca part dans tous les sens. Je vais essayer avec un autre layout de circuit. \"Poster V5\" Je me suis ensuite dit que le circuit n'\u00e9tait peut \u00eatre tout simplement pas une bonne id\u00e9e. J'ai donc essay\u00e9 de faire quelque chose de plus classique avec juste un peu de background pour qu'on puisse \u00e9viter le soucis de la page blanche derri\u00e8re : \"Poster V6\" Puis je me suis dit que finalement le circuit me manquait. Alors j'ai d\u00e9cid\u00e9 de combiner le background et le circuit ainsi que simplifier l\u00e9g\u00e8rement les diagrammes en retouchant un peu tout le reste on pouvait arriver \u00e0 quelque chose de sympatique : \"Poster V7\" Je ne suis pas content \u00e0 100% mais bon je pense que je vais m'en satisfaire. Pour donner une id\u00e9e de la gal\u00e8re que c'est de cr\u00e9er un poster, voici ce \u00e0 quoi ressemble mon espace de travail Figma : \"Bordel Figma\" Je ne suis pas un graphiste et ca se voit '^^. Je pense que comme il me reste un peu de temps aujourd'hui, je vais faire un peu de documentation de la partie r\u00e9cup\u00e8ration d'images. En effet, je pense que je n'aurai plus besoin de changer grand chose \u00e0 ce niveau. Mais je ne ferai pas la partie analyse fonctionnelle car l'interface n'est clairement pas termin\u00e9e. En fait j'avais oubli\u00e9 mais j'ai eu un rendez vous m\u00e9dical du coup je n'ai pas eu trop le temps de faire la doc que je voulais. Mais au moins je pense avoir finit mon travail sur le poster et le abstract en Anglais qui sont les deux gros livrables \u00e0 venir.","title":"Lundi 8 Mai 2023"},{"location":"jdb.html#mardi-9-mai-2023","text":"Bon je viens de me rendre compte que apparemment on doit rendre l'abstract anglais, le Poster, ET LE PROJET. Je pense que mes deux jours \u00e0 l'arm\u00e9e m'ont fait perdre un peu la notion du temps car j'avais l'impression que l'evaluation interm\u00e9diaire 1 \u00e9tait il y a genre moins d'une semaine. Donc aujourd'hui je ne vais pas trop avancer sur le code et vraiment me focus sur la documentation de la r\u00e9cup\u00e8ration d'images. Je pense que je vais aussi ajouter la partie calibration \u00e0 la documentation. Je pense que c'est important que je prenne le temps maintenant car sinon le prof aura l'impression que ca n'a pas trop avanc\u00e9 depuis la derni\u00e8re fois. Et puis je pense que la partie calibration et r\u00e9cup\u00e8ration d'images ne va pas trop changer et la partie calibration encore moins. La partie anglaise je fais la revoir un peu mais je l'avais d\u00e9ja faite pendant les premiers jours alors ca devrait aller. Pour le rendu il nous \u00e9tait demand\u00e9 de fournir un fichier PDF avec tout dedans avec une table des mati\u00e8res notre code source etc... Pour ce faire j'ai du changer le mkdocs.yml et installer des packages. Voici les changements :: site_name: Documentation Track Trends site_author: Rohmer Maxime copyright: \u00a9CFPTI Tech2 theme: name: material palette: # Palette toggle for light mode - media: \"(prefers-color-scheme: light)\" scheme: default toggle: icon: material/brightness-7 name: Switch to dark mode # Palette toggle for dark mode - media: \"(prefers-color-scheme: dark)\" scheme: slate toggle: icon: material/brightness-4 name: Switch to light mode markdown_extensions: - attr_list - md_in_html - pymdownx.highlight plugins: - glightbox - search - img2fig - with-pdf: cover_subtitle: Vroum Vroum enabled_if_env: ENABLE_PDF_EXPORT - annexes-integration: annexes: # Required (at least 1) - ConfigurationTool.cs: Code/ConfigurationTool.cs # An path to an annex with its title - DriverGapToLeaderWindow.cs: Code/DriverGapToLeaderWindow.cs # An path to an annex with its title - DriverPositionWindow.cs: Code/DriverPositionWindow.cs # An path to an annex with its title - F1TVEmulator.cs: Code/F1TVEmulator.cs # An path to an annex with its title - Program.cs: Code/Program.cs # An path to an annex with its title - Window.cs: Code/Window.cs # An path to an annex with its title - DriverData.cs: Code/DriverData.cs # An path to an annex with its title - DriverLapTimeWindow.cs: Code/DriverLapTimeWindow.cs # An path to an annex with its title - DriverSectorWindow.cs: Code/DriverSectorWindow.cs # An path to an annex with its title - Form1.cs: Code/Form1.cs # An path to an annex with its title - Reader.cs: Code/Reader.cs # An path to an annex with its title - Zone.cs: Code/Zone.cs # An path to an annex with its title - DriverDrsWindow.cs: Code/DriverDrsWindow.cs # An path to an annex with its title - DriverNameWindow.cs: Code/DriverNameWindow.cs # An path to an annex with its title - DriverTyresWindow.cs: Code/DriverTyresWindow.cs # An path to an annex with its title - OcrImage.cs: Code/OcrImage.cs # An path to an annex with its title - Settings.cs: Code/Settings.cs # An path to an annex with its title - recoverCookiesCSV.py: Code/recoverCookiesCSV.py # An path to an annex with its title Je remercie Monsieur Briard le sultan officiel de Mkdocs de la classe de m'avoir aid\u00e9 pour cette partie et avoir cr\u00e9\u00e9 un plugin qui me permet de mettre mon code source directement dans le pdf. Bon au final j'ai quand m\u00eame chang\u00e9 mon poster \"Poster V8\" Mais je suis trop attach\u00e9 \u00e0 l'ancien concept alors je vais plut\u00f4t utiliser ca : \"Poster V9\"","title":"Mardi 9 Mai 2023"},{"location":"Code/ConfigurationTool.html","text":"ConfigurationTool.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : ConfigurationTool.cs /// Brief : Class that contains all the methods needed to create a config file for the OCR /// Version : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using Tesseract; using System.IO; namespace Test_Merge { public class ConfigurationTool { public Zone MainZone; public const int NUMBER_OF_DRIVERS = 20; public const int NUMBER_OF_ZONES = 9; public const string CONFIGS_FOLDER_NAME = \"./Presets/\"; public ConfigurationTool(Bitmap fullImage, Rectangle mainZoneDimensions) { MainZone = new Zone(fullImage, mainZoneDimensions,\"Main\"); AutoCalibrate(); } public void ResetMainZone() { MainZone.ResetZones(); } public void ResetWindows() { MainZone.ResetWindows(); } public void SaveToJson(List drivers, string configName) { string JSON = \"\"; JSON += \"{\" + Environment.NewLine; JSON += MainZone.ToJSON() + \",\" + Environment.NewLine; JSON += \"\\\"Drivers\\\":[\" + Environment.NewLine; for (int i = 0; i < drivers.Count; i++) { JSON += \"\\\"\" + drivers[i] + \"\\\"\"; if (i < drivers.Count - 1) JSON += \",\"; JSON += Environment.NewLine; } JSON += \"]\" + Environment.NewLine; JSON += \"}\"; if (!Directory.Exists(CONFIGS_FOLDER_NAME)) Directory.CreateDirectory(CONFIGS_FOLDER_NAME); string path = CONFIGS_FOLDER_NAME + configName; if (File.Exists(path + \".json\")) { //We need to create a new name int count = 2; while (File.Exists(path + \"_\" + count + \".json\")) { count++; } path += \"_\" + count + \".json\"; } else { path += \".json\"; } File.WriteAllText(path, JSON); } public void AddWindows(List rectangles) { foreach (Zone driverZone in MainZone.Zones) { Bitmap zoneImage = driverZone.ZoneImage; for (int i = 1; i <= rectangles.Count; i++) { switch (i) { case 1: //First zone should be the driver's Position driverZone.AddWindow(new DriverPositionWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 2: //First zone should be the Gap to leader driverZone.AddWindow(new DriverGapToLeaderWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 3: //First zone should be the driver's Lap Time driverZone.AddWindow(new DriverLapTimeWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 4: //First zone should be the driver's DRS status driverZone.AddWindow(new DriverDrsWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 5: //First zone should be the driver's Tyre's informations driverZone.AddWindow(new DriverTyresWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 6: //First zone should be the driver's Name driverZone.AddWindow(new DriverNameWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 7: //First zone should be the driver's First Sector driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 1, false)); break; case 8: //First zone should be the driver's Second Sector driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 2, false)); break; case 9: //First zone should be the driver's Position Sector driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 3, false)); break; } } } } public void AutoCalibrate() { List detectedText = new List(); List zones = new List(); TesseractEngine engine = new TesseractEngine(Window.TESS_DATA_FOLDER.FullName, \"eng\", EngineMode.Default); Image image = MainZone.ZoneImage; var tessImage = Pix.LoadFromMemory(Window.ImageToByte(image)); Page page = engine.Process(tessImage); using (var iter = page.GetIterator()) { iter.Begin(); do { Rect boundingBox; if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox)) { //var text = iter.GetText(PageIteratorLevel.Word).ToUpper(); //We remove all the rectangles that are definitely too big if (boundingBox.Height < image.Height / NUMBER_OF_DRIVERS) { //Now we add a filter to only get the boxes in the right because they are much more reliable in size if (boundingBox.X1 > image.Width / 2) { //Now we check if an other square box has been found roughly in the same y axis bool match = false; //The tolerance is roughly half the size that a window will be int tolerance = (image.Height / NUMBER_OF_DRIVERS) / 2; foreach (Rectangle rect in detectedText) { if (rect.Y > boundingBox.Y1 - tolerance && rect.Y < boundingBox.Y1 + tolerance) { //There already is a rectangle in this line match = true; } } //if nothing matched we can add it if (!match) detectedText.Add(new Rectangle(boundingBox.X1, boundingBox.Y1, boundingBox.Width, boundingBox.Height)); } } } } while (iter.Next(PageIteratorLevel.Word)); } //DEBUG int i = 1; foreach (Rectangle Rectangle in detectedText) { Rectangle windowRectangle; Size windowSize = new Size(image.Width, image.Height / NUMBER_OF_DRIVERS); Point windowLocation = new Point(0, (Rectangle.Y + Rectangle.Height / 2) - windowSize.Height / 2); windowRectangle = new Rectangle(windowLocation, windowSize); //We add the driver zones Zone driverZone = new Zone(MainZone.ZoneImage, windowRectangle, \"DriverZone\"); MainZone.AddZone(driverZone); driverZone.ZoneImage.Save(\"Driver\" + i+\".png\"); i++; } } } }","title":"ConfigurationTool.cs"},{"location":"Code/ConfigurationTool.html#configurationtoolcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : ConfigurationTool.cs /// Brief : Class that contains all the methods needed to create a config file for the OCR /// Version : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using Tesseract; using System.IO; namespace Test_Merge { public class ConfigurationTool { public Zone MainZone; public const int NUMBER_OF_DRIVERS = 20; public const int NUMBER_OF_ZONES = 9; public const string CONFIGS_FOLDER_NAME = \"./Presets/\"; public ConfigurationTool(Bitmap fullImage, Rectangle mainZoneDimensions) { MainZone = new Zone(fullImage, mainZoneDimensions,\"Main\"); AutoCalibrate(); } public void ResetMainZone() { MainZone.ResetZones(); } public void ResetWindows() { MainZone.ResetWindows(); } public void SaveToJson(List drivers, string configName) { string JSON = \"\"; JSON += \"{\" + Environment.NewLine; JSON += MainZone.ToJSON() + \",\" + Environment.NewLine; JSON += \"\\\"Drivers\\\":[\" + Environment.NewLine; for (int i = 0; i < drivers.Count; i++) { JSON += \"\\\"\" + drivers[i] + \"\\\"\"; if (i < drivers.Count - 1) JSON += \",\"; JSON += Environment.NewLine; } JSON += \"]\" + Environment.NewLine; JSON += \"}\"; if (!Directory.Exists(CONFIGS_FOLDER_NAME)) Directory.CreateDirectory(CONFIGS_FOLDER_NAME); string path = CONFIGS_FOLDER_NAME + configName; if (File.Exists(path + \".json\")) { //We need to create a new name int count = 2; while (File.Exists(path + \"_\" + count + \".json\")) { count++; } path += \"_\" + count + \".json\"; } else { path += \".json\"; } File.WriteAllText(path, JSON); } public void AddWindows(List rectangles) { foreach (Zone driverZone in MainZone.Zones) { Bitmap zoneImage = driverZone.ZoneImage; for (int i = 1; i <= rectangles.Count; i++) { switch (i) { case 1: //First zone should be the driver's Position driverZone.AddWindow(new DriverPositionWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 2: //First zone should be the Gap to leader driverZone.AddWindow(new DriverGapToLeaderWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 3: //First zone should be the driver's Lap Time driverZone.AddWindow(new DriverLapTimeWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 4: //First zone should be the driver's DRS status driverZone.AddWindow(new DriverDrsWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 5: //First zone should be the driver's Tyre's informations driverZone.AddWindow(new DriverTyresWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 6: //First zone should be the driver's Name driverZone.AddWindow(new DriverNameWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 7: //First zone should be the driver's First Sector driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 1, false)); break; case 8: //First zone should be the driver's Second Sector driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 2, false)); break; case 9: //First zone should be the driver's Position Sector driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 3, false)); break; } } } } public void AutoCalibrate() { List detectedText = new List(); List zones = new List(); TesseractEngine engine = new TesseractEngine(Window.TESS_DATA_FOLDER.FullName, \"eng\", EngineMode.Default); Image image = MainZone.ZoneImage; var tessImage = Pix.LoadFromMemory(Window.ImageToByte(image)); Page page = engine.Process(tessImage); using (var iter = page.GetIterator()) { iter.Begin(); do { Rect boundingBox; if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox)) { //var text = iter.GetText(PageIteratorLevel.Word).ToUpper(); //We remove all the rectangles that are definitely too big if (boundingBox.Height < image.Height / NUMBER_OF_DRIVERS) { //Now we add a filter to only get the boxes in the right because they are much more reliable in size if (boundingBox.X1 > image.Width / 2) { //Now we check if an other square box has been found roughly in the same y axis bool match = false; //The tolerance is roughly half the size that a window will be int tolerance = (image.Height / NUMBER_OF_DRIVERS) / 2; foreach (Rectangle rect in detectedText) { if (rect.Y > boundingBox.Y1 - tolerance && rect.Y < boundingBox.Y1 + tolerance) { //There already is a rectangle in this line match = true; } } //if nothing matched we can add it if (!match) detectedText.Add(new Rectangle(boundingBox.X1, boundingBox.Y1, boundingBox.Width, boundingBox.Height)); } } } } while (iter.Next(PageIteratorLevel.Word)); } //DEBUG int i = 1; foreach (Rectangle Rectangle in detectedText) { Rectangle windowRectangle; Size windowSize = new Size(image.Width, image.Height / NUMBER_OF_DRIVERS); Point windowLocation = new Point(0, (Rectangle.Y + Rectangle.Height / 2) - windowSize.Height / 2); windowRectangle = new Rectangle(windowLocation, windowSize); //We add the driver zones Zone driverZone = new Zone(MainZone.ZoneImage, windowRectangle, \"DriverZone\"); MainZone.AddZone(driverZone); driverZone.ZoneImage.Save(\"Driver\" + i+\".png\"); i++; } } } }","title":"ConfigurationTool.cs"},{"location":"Code/DriverGapToLeaderWindow.html","text":"DriverGapToLeaderWindow.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverGapToLeaderWindow.cs /// Brief : Window containing infos about the gap to the leader of a driver /// Version : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Test_Merge { internal class DriverGapToLeaderWindow:Window { public DriverGapToLeaderWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"GapToLeader\"; } /// /// Decodes the gap to leader using Tesseract OCR /// /// public override async Task DecodePng() { int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Gap, Engine); return result; } } }","title":"DriverGapToLeaderWindow.cs"},{"location":"Code/DriverGapToLeaderWindow.html#drivergaptoleaderwindowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverGapToLeaderWindow.cs /// Brief : Window containing infos about the gap to the leader of a driver /// Version : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Test_Merge { internal class DriverGapToLeaderWindow:Window { public DriverGapToLeaderWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"GapToLeader\"; } /// /// Decodes the gap to leader using Tesseract OCR /// /// public override async Task DecodePng() { int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Gap, Engine); return result; } } }","title":"DriverGapToLeaderWindow.cs"},{"location":"Code/DriverPositionWindow.html","text":"DriverPositionWindow.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverPosition.cs /// Brief : Window containing infos about the position of a driver. /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { public class DriverPositionWindow:Window { public DriverPositionWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Position\"; } /// /// Decodes the position number using Tesseract OCR /// /// The position of the pilot in int public override async Task DecodePng() { string ocrResult = await GetStringFromPng(WindowImage, Engine, \"0123456789\"); int position; try { position = Convert.ToInt32(ocrResult); } catch { position = -1; } return position; } } }","title":"DriverPositionWindow.cs"},{"location":"Code/DriverPositionWindow.html#driverpositionwindowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverPosition.cs /// Brief : Window containing infos about the position of a driver. /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { public class DriverPositionWindow:Window { public DriverPositionWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Position\"; } /// /// Decodes the position number using Tesseract OCR /// /// The position of the pilot in int public override async Task DecodePng() { string ocrResult = await GetStringFromPng(WindowImage, Engine, \"0123456789\"); int position; try { position = Convert.ToInt32(ocrResult); } catch { position = -1; } return position; } } }","title":"DriverPositionWindow.cs"},{"location":"Code/F1TVEmulator.html","text":"F1TVEmulator.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : F1TVEmulator.cs /// Brief : Class that contains methods to emulate a browser and navigate the F1TV website /// Version : 0.1 using OpenQA.Selenium; using OpenQA.Selenium.Firefox; using OpenQA.Selenium.Interactions; using OpenQA.Selenium.Support.UI; using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Test_Merge { internal class F1TVEmulator { public const string COOKIE_HOST = \".formula1.com\"; public const string PYTHON_COOKIE_RETRIEVAL_FILENAME = \"recoverCookiesCSV.py\"; public const string GECKODRIVER_FILENAME = @\"geckodriver-v0.27.0-win64\\geckodriver.exe\"; //BE CAREFULL IF YOU CHANGE IT HERE YOU NEED TO CHANGE IT IN THE PYTHON SCRIPT TOO public const string COOKIES_CSV_FILENAME = \"cookies.csv\"; private FirefoxDriver Driver; private bool _ready; private string _grandPrixUrl; public string GrandPrixUrl { get => _grandPrixUrl; private set => _grandPrixUrl = value; } public bool Ready { get => _ready; set => _ready = value; } public F1TVEmulator(string grandPrixUrl) { GrandPrixUrl = grandPrixUrl; Ready = false; } private void StartCookieRecovering() { string scriptPath = PYTHON_COOKIE_RETRIEVAL_FILENAME; Process process = new Process(); process.StartInfo.FileName = \"python.exe\"; process.StartInfo.Arguments = scriptPath; process.StartInfo.UseShellExecute = false; process.StartInfo.RedirectStandardOutput = true; process.Start(); string output = process.StandardOutput.ReadToEnd(); process.WaitForExit(); } public string GetCookie(string host, string name) { StartCookieRecovering(); string value = \"\"; List cookies = new List(); using (var reader = new StreamReader(COOKIES_CSV_FILENAME)) { // Read the header row and validate column order string header = reader.ReadLine(); string[] expectedColumns = { \"host_key\", \"name\", \"value\", \"path\", \"expires_utc\", \"is_secure\", \"is_httponly\" }; string[] actualColumns = header.Split(','); for (int i = 0; i < expectedColumns.Length; i++) { if (expectedColumns[i] != actualColumns[i]) { throw new InvalidOperationException($\"Expected column '{expectedColumns[i]}' at index {i} but found '{actualColumns[i]}'\"); } } // Read each data row and parse values into a Cookie object while (!reader.EndOfStream) { string line = reader.ReadLine(); string[] fields = line.Split(','); string hostname = fields[0]; string cookieName = fields[1]; if (hostname == host && cookieName == name) { value = fields[2]; } } } return value; } public async Task Start() { Ready = false; string loginCookieName = \"login\"; string loginSessionCookieName = \"login-session\"; string loginCookieValue = GetCookie(COOKIE_HOST, loginCookieName); string loginSessionValue = GetCookie(COOKIE_HOST, loginSessionCookieName); int windowWidth = 1920; int windowHeight = 768; var service = FirefoxDriverService.CreateDefaultService(GECKODRIVER_FILENAME); service.Host = \"127.0.0.1\"; service.Port = 5555; FirefoxProfile profile = new FirefoxProfile(); FirefoxOptions options = new FirefoxOptions(); //profile.SetPreference(\"full-screen-api.ignore-widgets\", true); //profile.SetPreference(\"media.hardware-video-decoding.enabled\", true); //profile.SetPreference(\"full-screen-api.enabled\", true); options.Profile = profile; profile.SetPreference(\"layout.css.devPixelsPerPx\", \"1.0\"); options.AcceptInsecureCertificates = true; options.AddArgument(\"--headless\"); //options.AddArgument(\"--start-maximized\"); //options.AddArgument(\"--window-size=1920x1080\"); //options.AddArgument(\"--width=\" + windowWidth); //options.AddArgument(\"--height=\" + windowHeight); //options.AddArgument(\"-window-size=1920x1080\"); //options.AddArgument(\"--width=1920\"); //options.AddArgument(\"--height=1080\"); //profile try { Driver = new FirefoxDriver(service, options); } catch { Ready = false; return 101; } Actions actions = new Actions(Driver); var loginCookie = new Cookie(loginCookieName, loginCookieValue, COOKIE_HOST, \"/\", DateTime.Now.AddDays(5)); var loginSessionCookie = new Cookie(loginSessionCookieName, loginSessionValue, COOKIE_HOST, \"/\", DateTime.Now.AddDays(5)); Driver.Navigate().GoToUrl(\"https://f1tv.formula1.com/\"); Driver.Manage().Cookies.AddCookie(loginCookie); Driver.Manage().Cookies.AddCookie(loginSessionCookie); try { Driver.Navigate().GoToUrl(GrandPrixUrl); } catch { //The url is not a valid url Driver.Dispose(); return 103; } //Waits for the page to fully load Driver.Manage().Timeouts().PageLoad = TimeSpan.FromSeconds(30); //Removes the cookie prompt try { IWebElement conscentButton = Driver.FindElement(By.Id(\"truste-consent-button\")); conscentButton.Click(); } catch { //Could not locate the cookie button Screenshot(\"ERROR104\"); Driver.Dispose(); return 104; } //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); //Switches to the Data channel try { IWebElement dataChannelButton = Driver.FindElement(By.ClassName(\"data-button\")); dataChannelButton.Click(); } catch { //If the data button does not exists its because the user is not connected Screenshot(\"ERROR102\"); Driver.Dispose(); return 102; } //Open settings // Press the space key, this should make the setting button visible // It does not matter if the feed is paused because when changing channel it autoplays actions.SendKeys(OpenQA.Selenium.Keys.Space).Perform(); //Clicks on the settings Icon int tries = 0; bool success = false; while (tries < 100 && !success) { Thread.Sleep(100); try { IWebElement settingsButton = Driver.FindElement(By.ClassName(\"bmpui-ui-settingstogglebutton\")); settingsButton.Click(); IWebElement selectElement = Driver.FindElement(By.ClassName(\"bmpui-ui-videoqualityselectbox\")); SelectElement select = new SelectElement(selectElement); IWebElement selectOption = selectElement.FindElement(By.CssSelector(\"option[value^='1080_']\")); selectOption.Click(); success = true; } catch { //Sometimes it can crash because it could not get the options to show up in time. When it happens just retry success = false; tries++; } } if (!success) { Screenshot(\"ERROR105\"); Driver.Dispose(); return 105; } Screenshot(\"BEFOREFULLSCREEN\"); //Makes the feed fullscreen //Driver.Manage().Window.Size = new System.Drawing.Size(windowWidth, windowHeight); Driver.Manage().Window.Maximize(); WebDriverWait wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); try { IWebElement fullScreenButton = Driver.FindElement(By.ClassName(\"bmpui-ui-fullscreentogglebutton\")); fullScreenButton.Click(); } catch { Screenshot(\"ERROR106\"); Driver.Dispose(); return 106; } Screenshot(\"AFTERFULLSCREEN\"); //STARTUP FINISHED READY TO SCREENSHOT Ready = true; return 0; } public Bitmap Screenshot(string name = \"TEST\") { Bitmap result = new Bitmap(4242, 6969); try { //Screenshot scrsht = ((ITakesScreenshot)Driver).GetScreenshot(); //profileriver.SetPreference(\"layout.css.devPixelsPerPx\", \"1.0\"); //Screenshot scrsht = Driver.GetFullPageScreenshot(); Screenshot scrsht = Driver.GetScreenshot(); byte[] screenshotBytes = Convert.FromBase64String(scrsht.AsBase64EncodedString); MemoryStream stream = new MemoryStream(screenshotBytes); result = new Bitmap(stream); //result.Save(name + \".png\"); scrsht.SaveAsFile(name + \".png\"); } catch { //Nothing for now } return result; } public void Stop() { Ready = false; Driver.Dispose(); } public void ResetDriver() { Ready = false; Driver.Dispose(); Driver = null; } } }","title":"F1TVEmulator.cs"},{"location":"Code/F1TVEmulator.html#f1tvemulatorcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : F1TVEmulator.cs /// Brief : Class that contains methods to emulate a browser and navigate the F1TV website /// Version : 0.1 using OpenQA.Selenium; using OpenQA.Selenium.Firefox; using OpenQA.Selenium.Interactions; using OpenQA.Selenium.Support.UI; using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Test_Merge { internal class F1TVEmulator { public const string COOKIE_HOST = \".formula1.com\"; public const string PYTHON_COOKIE_RETRIEVAL_FILENAME = \"recoverCookiesCSV.py\"; public const string GECKODRIVER_FILENAME = @\"geckodriver-v0.27.0-win64\\geckodriver.exe\"; //BE CAREFULL IF YOU CHANGE IT HERE YOU NEED TO CHANGE IT IN THE PYTHON SCRIPT TOO public const string COOKIES_CSV_FILENAME = \"cookies.csv\"; private FirefoxDriver Driver; private bool _ready; private string _grandPrixUrl; public string GrandPrixUrl { get => _grandPrixUrl; private set => _grandPrixUrl = value; } public bool Ready { get => _ready; set => _ready = value; } public F1TVEmulator(string grandPrixUrl) { GrandPrixUrl = grandPrixUrl; Ready = false; } private void StartCookieRecovering() { string scriptPath = PYTHON_COOKIE_RETRIEVAL_FILENAME; Process process = new Process(); process.StartInfo.FileName = \"python.exe\"; process.StartInfo.Arguments = scriptPath; process.StartInfo.UseShellExecute = false; process.StartInfo.RedirectStandardOutput = true; process.Start(); string output = process.StandardOutput.ReadToEnd(); process.WaitForExit(); } public string GetCookie(string host, string name) { StartCookieRecovering(); string value = \"\"; List cookies = new List(); using (var reader = new StreamReader(COOKIES_CSV_FILENAME)) { // Read the header row and validate column order string header = reader.ReadLine(); string[] expectedColumns = { \"host_key\", \"name\", \"value\", \"path\", \"expires_utc\", \"is_secure\", \"is_httponly\" }; string[] actualColumns = header.Split(','); for (int i = 0; i < expectedColumns.Length; i++) { if (expectedColumns[i] != actualColumns[i]) { throw new InvalidOperationException($\"Expected column '{expectedColumns[i]}' at index {i} but found '{actualColumns[i]}'\"); } } // Read each data row and parse values into a Cookie object while (!reader.EndOfStream) { string line = reader.ReadLine(); string[] fields = line.Split(','); string hostname = fields[0]; string cookieName = fields[1]; if (hostname == host && cookieName == name) { value = fields[2]; } } } return value; } public async Task Start() { Ready = false; string loginCookieName = \"login\"; string loginSessionCookieName = \"login-session\"; string loginCookieValue = GetCookie(COOKIE_HOST, loginCookieName); string loginSessionValue = GetCookie(COOKIE_HOST, loginSessionCookieName); int windowWidth = 1920; int windowHeight = 768; var service = FirefoxDriverService.CreateDefaultService(GECKODRIVER_FILENAME); service.Host = \"127.0.0.1\"; service.Port = 5555; FirefoxProfile profile = new FirefoxProfile(); FirefoxOptions options = new FirefoxOptions(); //profile.SetPreference(\"full-screen-api.ignore-widgets\", true); //profile.SetPreference(\"media.hardware-video-decoding.enabled\", true); //profile.SetPreference(\"full-screen-api.enabled\", true); options.Profile = profile; profile.SetPreference(\"layout.css.devPixelsPerPx\", \"1.0\"); options.AcceptInsecureCertificates = true; options.AddArgument(\"--headless\"); //options.AddArgument(\"--start-maximized\"); //options.AddArgument(\"--window-size=1920x1080\"); //options.AddArgument(\"--width=\" + windowWidth); //options.AddArgument(\"--height=\" + windowHeight); //options.AddArgument(\"-window-size=1920x1080\"); //options.AddArgument(\"--width=1920\"); //options.AddArgument(\"--height=1080\"); //profile try { Driver = new FirefoxDriver(service, options); } catch { Ready = false; return 101; } Actions actions = new Actions(Driver); var loginCookie = new Cookie(loginCookieName, loginCookieValue, COOKIE_HOST, \"/\", DateTime.Now.AddDays(5)); var loginSessionCookie = new Cookie(loginSessionCookieName, loginSessionValue, COOKIE_HOST, \"/\", DateTime.Now.AddDays(5)); Driver.Navigate().GoToUrl(\"https://f1tv.formula1.com/\"); Driver.Manage().Cookies.AddCookie(loginCookie); Driver.Manage().Cookies.AddCookie(loginSessionCookie); try { Driver.Navigate().GoToUrl(GrandPrixUrl); } catch { //The url is not a valid url Driver.Dispose(); return 103; } //Waits for the page to fully load Driver.Manage().Timeouts().PageLoad = TimeSpan.FromSeconds(30); //Removes the cookie prompt try { IWebElement conscentButton = Driver.FindElement(By.Id(\"truste-consent-button\")); conscentButton.Click(); } catch { //Could not locate the cookie button Screenshot(\"ERROR104\"); Driver.Dispose(); return 104; } //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); //Switches to the Data channel try { IWebElement dataChannelButton = Driver.FindElement(By.ClassName(\"data-button\")); dataChannelButton.Click(); } catch { //If the data button does not exists its because the user is not connected Screenshot(\"ERROR102\"); Driver.Dispose(); return 102; } //Open settings // Press the space key, this should make the setting button visible // It does not matter if the feed is paused because when changing channel it autoplays actions.SendKeys(OpenQA.Selenium.Keys.Space).Perform(); //Clicks on the settings Icon int tries = 0; bool success = false; while (tries < 100 && !success) { Thread.Sleep(100); try { IWebElement settingsButton = Driver.FindElement(By.ClassName(\"bmpui-ui-settingstogglebutton\")); settingsButton.Click(); IWebElement selectElement = Driver.FindElement(By.ClassName(\"bmpui-ui-videoqualityselectbox\")); SelectElement select = new SelectElement(selectElement); IWebElement selectOption = selectElement.FindElement(By.CssSelector(\"option[value^='1080_']\")); selectOption.Click(); success = true; } catch { //Sometimes it can crash because it could not get the options to show up in time. When it happens just retry success = false; tries++; } } if (!success) { Screenshot(\"ERROR105\"); Driver.Dispose(); return 105; } Screenshot(\"BEFOREFULLSCREEN\"); //Makes the feed fullscreen //Driver.Manage().Window.Size = new System.Drawing.Size(windowWidth, windowHeight); Driver.Manage().Window.Maximize(); WebDriverWait wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); try { IWebElement fullScreenButton = Driver.FindElement(By.ClassName(\"bmpui-ui-fullscreentogglebutton\")); fullScreenButton.Click(); } catch { Screenshot(\"ERROR106\"); Driver.Dispose(); return 106; } Screenshot(\"AFTERFULLSCREEN\"); //STARTUP FINISHED READY TO SCREENSHOT Ready = true; return 0; } public Bitmap Screenshot(string name = \"TEST\") { Bitmap result = new Bitmap(4242, 6969); try { //Screenshot scrsht = ((ITakesScreenshot)Driver).GetScreenshot(); //profileriver.SetPreference(\"layout.css.devPixelsPerPx\", \"1.0\"); //Screenshot scrsht = Driver.GetFullPageScreenshot(); Screenshot scrsht = Driver.GetScreenshot(); byte[] screenshotBytes = Convert.FromBase64String(scrsht.AsBase64EncodedString); MemoryStream stream = new MemoryStream(screenshotBytes); result = new Bitmap(stream); //result.Save(name + \".png\"); scrsht.SaveAsFile(name + \".png\"); } catch { //Nothing for now } return result; } public void Stop() { Ready = false; Driver.Dispose(); } public void ResetDriver() { Ready = false; Driver.Dispose(); Driver = null; } } }","title":"F1TVEmulator.cs"},{"location":"Code/Program.html","text":"Program.cs \ufeffusing System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Windows.Forms; namespace Test_Merge { internal static class Program { /// /// The main entry point for the application. /// [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } } }","title":"Program.cs"},{"location":"Code/Program.html#programcs","text":"\ufeffusing System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Windows.Forms; namespace Test_Merge { internal static class Program { /// /// The main entry point for the application. /// [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } } }","title":"Program.cs"},{"location":"Code/Window.html","text":"Window.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : Window.cs /// Brief : Default Window object that is mainly expected to be inherited. /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; using System.IO; using Tesseract; using System.Text.RegularExpressions; using System.Drawing.Drawing2D; namespace Test_Merge { public class Window { private Rectangle _bounds; private Bitmap _image; private string _name; protected TesseractEngine Engine; public Rectangle Bounds { get => _bounds; private set => _bounds = value; } public Bitmap Image { get => _image; set => _image = value; } public string Name { get => _name; protected set => _name = value; } //This will have to be changed if you want to make it run on your machine public static DirectoryInfo TESS_DATA_FOLDER = new DirectoryInfo(@\"C:\\Users\\Moi\\Pictures\\SeleniumScreens\\TessData\"); public Bitmap WindowImage { get { //This little trickery lets you have the image that the window sees Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); return sample; } } public Window(Bitmap image, Rectangle bounds, bool generateEngine = true) { Image = image; Bounds = bounds; if (generateEngine) { Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, \"eng\", EngineMode.Default); Engine.DefaultPageSegMode = PageSegMode.SingleLine; } } /// /// Method that will have to be used by the childrens to let the model make them decode the images they have /// /// Returns an object because we dont know what kind of return it will be public virtual async Task DecodePng() { return \"NaN\"; } /// /// Method that will have to be used by the childrens to let the model make them decode the images they have /// /// This is a list of the different possible drivers in the race. It should not be too big but NEVER be too short /// Returns an object because we dont know what kind of return it will be public virtual async Task DecodePng(List driverList) { return \"NaN\"; } /// /// This converts an image into a byte[]. It can be usefull when doing unsafe stuff. Use at your own risks /// /// The image you want to convert /// A byte array containing the image informations public static byte[] ImageToByte(Image inputImage) { using (var stream = new MemoryStream()) { inputImage.Save(stream, System.Drawing.Imaging.ImageFormat.Png); return stream.ToArray(); } } /// /// This method is used to recover a time from a PNG using Tesseract OCR /// /// The image where the text is /// The type of window it is /// The Tesseract Engine /// The time in milliseconds public static async Task GetTimeFromPng(Bitmap windowImage, OcrImage.WindowType windowType, TesseractEngine Engine) { //Kind of a big method but it has a lot of error handling and has to work with three special cases string rawResult = \"\"; int result = 0; switch (windowType) { case OcrImage.WindowType.Sector: //The usual sector is in this form : 33.456 Engine.SetVariable(\"tessedit_char_whitelist\", \"0123456789.\"); break; case OcrImage.WindowType.LapTime: //The usual Lap time is in this form : 1:45:345 Engine.SetVariable(\"tessedit_char_whitelist\", \"0123456789.:\"); break; case OcrImage.WindowType.Gap: //The usual Gap is in this form : + 34.567 Engine.SetVariable(\"tessedit_char_whitelist\", \"0123456789.+\"); break; default: Engine.SetVariable(\"tessedit_char_whitelist\", \"\"); break; } Bitmap enhancedImage = new OcrImage(windowImage).Enhance(windowType); var tessImage = Pix.LoadFromMemory(ImageToByte(enhancedImage)); Page page = Engine.Process(tessImage); Graphics g = Graphics.FromImage(enhancedImage); // Get the iterator for the page layout using (var iter = page.GetIterator()) { // Loop over the elements of the page layout iter.Begin(); do { // Get the text for the current element try { rawResult += iter.GetText(PageIteratorLevel.Word); } catch { //nothing we just dont add it if its not a number } } while (iter.Next(PageIteratorLevel.Word)); } List rawNumbers; //In the gaps we can find '+' but we dont care about it its redondant a driver will never be - something if (windowType == OcrImage.WindowType.Gap) rawResult = Regex.Replace(rawResult, \"[^0-9.:]\", \"\"); //Splits into minuts seconds miliseconds rawNumbers = rawResult.Split('.', ':').ToList(); //removes any empty cells (tho this usually sign of a really bad OCR implementation tbh will have to be fixed higher in the chian) rawNumbers.RemoveAll(x => ((string)x) == \"\"); if (rawNumbers.Count == 3) { //mm:ss:ms result = (Convert.ToInt32(rawNumbers[0]) * 1000 * 60) + (Convert.ToInt32(rawNumbers[1]) * 1000) + Convert.ToInt32(rawNumbers[2]); } else { if (rawNumbers.Count == 2) { //ss:ms result = (Convert.ToInt32(rawNumbers[0]) * 1000) + Convert.ToInt32(rawNumbers[1]); if (result > 999999) { //We know that we have way too much seconds to make a minut //Its usually because the \":\" have been interpreted as a number int minuts = (int)(rawNumbers[0][0] - '0'); // rawNumbers[0][1] should contain the : that has been mistaken int seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString()); int ms = Convert.ToInt32(rawNumbers[1]); result = (Convert.ToInt32(minuts) * 1000 * 60) + (Convert.ToInt32(seconds) * 1000) + Convert.ToInt32(ms); } } else { if (rawNumbers.Count == 1) { try { result = Convert.ToInt32(rawNumbers[0]); } catch { //It can be because the input is empty or because its the LEADER bracket result = 0; } } else { //Auuuugh result = 0; } } } page.Dispose(); return result; } /// /// Method that recovers strings from an image using Tesseract OCR /// /// The image of the window that contains text /// The Tesseract engine /// The list of allowed chars /// The type of window the text is on. Depending on the context the OCR will behave differently /// the string it found public static async Task GetStringFromPng(Bitmap WindowImage, TesseractEngine Engine, string allowedChars = \"\", OcrImage.WindowType windowType = OcrImage.WindowType.Text) { string result = \"\"; Engine.SetVariable(\"tessedit_char_whitelist\", allowedChars); Bitmap rawData = WindowImage; Bitmap enhancedImage = new OcrImage(rawData).Enhance(windowType); Page page = Engine.Process(enhancedImage); using (var iter = page.GetIterator()) { iter.Begin(); do { result += iter.GetText(PageIteratorLevel.Word); } while (iter.Next(PageIteratorLevel.Word)); } page.Dispose(); return result; } /// /// Get a smaller image from a bigger one /// /// The big bitmap you want to get a part of /// The dimensions of the new bitmap /// The little bitmap protected Bitmap GetSmallBitmapFromBigOne(Bitmap inputBitmap, Rectangle newBitmapDimensions) { Bitmap sample = new Bitmap(newBitmapDimensions.Width, newBitmapDimensions.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(inputBitmap, new Rectangle(0, 0, sample.Width, sample.Height), newBitmapDimensions, GraphicsUnit.Pixel); return sample; } /// /// Returns the closest string from a list of options /// /// an array of all the possibilities /// the string you want to compare /// The closest option protected static string FindClosestMatch(List options, string testString) { var closestMatch = \"\"; var closestDistance = int.MaxValue; foreach (var item in options) { var distance = LevenshteinDistance(item, testString); if (distance < closestDistance) { closestMatch = item; closestDistance = distance; } } return closestMatch; } //This method has been generated with the help of ChatGPT /// /// Method that computes a score of distance between two strings /// /// The first string (order irrelevant) /// The second string (order irrelevant) /// The levenshtein distance protected static int LevenshteinDistance(string string1, string string2) { if (string.IsNullOrEmpty(string1)) { return string.IsNullOrEmpty(string2) ? 0 : string2.Length; } if (string.IsNullOrEmpty(string2)) { return string.IsNullOrEmpty(string1) ? 0 : string1.Length; } var d = new int[string1.Length + 1, string2.Length + 1]; for (var i = 0; i <= string1.Length; i++) { d[i, 0] = i; } for (var j = 0; j <= string2.Length; j++) { d[0, j] = j; } for (var i = 1; i <= string1.Length; i++) { for (var j = 1; j <= string2.Length; j++) { var cost = (string1[i - 1] == string2[j - 1]) ? 0 : 1; d[i, j] = Math.Min(Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost); } } return d[string1.Length, string2.Length]; } public virtual string ToJSON() { string result = \"\"; result += \"\\\"\" + Name + \"\\\"\" + \":{\" + Environment.NewLine; result += \"\\t\" + \"\\\"x\\\":\" + Bounds.X + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"y\\\":\" + Bounds.Y + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"width\\\":\" + Bounds.Width + Environment.NewLine; result += \"}\"; return result; } } }","title":"Window.cs"},{"location":"Code/Window.html#windowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : Window.cs /// Brief : Default Window object that is mainly expected to be inherited. /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; using System.IO; using Tesseract; using System.Text.RegularExpressions; using System.Drawing.Drawing2D; namespace Test_Merge { public class Window { private Rectangle _bounds; private Bitmap _image; private string _name; protected TesseractEngine Engine; public Rectangle Bounds { get => _bounds; private set => _bounds = value; } public Bitmap Image { get => _image; set => _image = value; } public string Name { get => _name; protected set => _name = value; } //This will have to be changed if you want to make it run on your machine public static DirectoryInfo TESS_DATA_FOLDER = new DirectoryInfo(@\"C:\\Users\\Moi\\Pictures\\SeleniumScreens\\TessData\"); public Bitmap WindowImage { get { //This little trickery lets you have the image that the window sees Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); return sample; } } public Window(Bitmap image, Rectangle bounds, bool generateEngine = true) { Image = image; Bounds = bounds; if (generateEngine) { Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, \"eng\", EngineMode.Default); Engine.DefaultPageSegMode = PageSegMode.SingleLine; } } /// /// Method that will have to be used by the childrens to let the model make them decode the images they have /// /// Returns an object because we dont know what kind of return it will be public virtual async Task DecodePng() { return \"NaN\"; } /// /// Method that will have to be used by the childrens to let the model make them decode the images they have /// /// This is a list of the different possible drivers in the race. It should not be too big but NEVER be too short /// Returns an object because we dont know what kind of return it will be public virtual async Task DecodePng(List driverList) { return \"NaN\"; } /// /// This converts an image into a byte[]. It can be usefull when doing unsafe stuff. Use at your own risks /// /// The image you want to convert /// A byte array containing the image informations public static byte[] ImageToByte(Image inputImage) { using (var stream = new MemoryStream()) { inputImage.Save(stream, System.Drawing.Imaging.ImageFormat.Png); return stream.ToArray(); } } /// /// This method is used to recover a time from a PNG using Tesseract OCR /// /// The image where the text is /// The type of window it is /// The Tesseract Engine /// The time in milliseconds public static async Task GetTimeFromPng(Bitmap windowImage, OcrImage.WindowType windowType, TesseractEngine Engine) { //Kind of a big method but it has a lot of error handling and has to work with three special cases string rawResult = \"\"; int result = 0; switch (windowType) { case OcrImage.WindowType.Sector: //The usual sector is in this form : 33.456 Engine.SetVariable(\"tessedit_char_whitelist\", \"0123456789.\"); break; case OcrImage.WindowType.LapTime: //The usual Lap time is in this form : 1:45:345 Engine.SetVariable(\"tessedit_char_whitelist\", \"0123456789.:\"); break; case OcrImage.WindowType.Gap: //The usual Gap is in this form : + 34.567 Engine.SetVariable(\"tessedit_char_whitelist\", \"0123456789.+\"); break; default: Engine.SetVariable(\"tessedit_char_whitelist\", \"\"); break; } Bitmap enhancedImage = new OcrImage(windowImage).Enhance(windowType); var tessImage = Pix.LoadFromMemory(ImageToByte(enhancedImage)); Page page = Engine.Process(tessImage); Graphics g = Graphics.FromImage(enhancedImage); // Get the iterator for the page layout using (var iter = page.GetIterator()) { // Loop over the elements of the page layout iter.Begin(); do { // Get the text for the current element try { rawResult += iter.GetText(PageIteratorLevel.Word); } catch { //nothing we just dont add it if its not a number } } while (iter.Next(PageIteratorLevel.Word)); } List rawNumbers; //In the gaps we can find '+' but we dont care about it its redondant a driver will never be - something if (windowType == OcrImage.WindowType.Gap) rawResult = Regex.Replace(rawResult, \"[^0-9.:]\", \"\"); //Splits into minuts seconds miliseconds rawNumbers = rawResult.Split('.', ':').ToList(); //removes any empty cells (tho this usually sign of a really bad OCR implementation tbh will have to be fixed higher in the chian) rawNumbers.RemoveAll(x => ((string)x) == \"\"); if (rawNumbers.Count == 3) { //mm:ss:ms result = (Convert.ToInt32(rawNumbers[0]) * 1000 * 60) + (Convert.ToInt32(rawNumbers[1]) * 1000) + Convert.ToInt32(rawNumbers[2]); } else { if (rawNumbers.Count == 2) { //ss:ms result = (Convert.ToInt32(rawNumbers[0]) * 1000) + Convert.ToInt32(rawNumbers[1]); if (result > 999999) { //We know that we have way too much seconds to make a minut //Its usually because the \":\" have been interpreted as a number int minuts = (int)(rawNumbers[0][0] - '0'); // rawNumbers[0][1] should contain the : that has been mistaken int seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString()); int ms = Convert.ToInt32(rawNumbers[1]); result = (Convert.ToInt32(minuts) * 1000 * 60) + (Convert.ToInt32(seconds) * 1000) + Convert.ToInt32(ms); } } else { if (rawNumbers.Count == 1) { try { result = Convert.ToInt32(rawNumbers[0]); } catch { //It can be because the input is empty or because its the LEADER bracket result = 0; } } else { //Auuuugh result = 0; } } } page.Dispose(); return result; } /// /// Method that recovers strings from an image using Tesseract OCR /// /// The image of the window that contains text /// The Tesseract engine /// The list of allowed chars /// The type of window the text is on. Depending on the context the OCR will behave differently /// the string it found public static async Task GetStringFromPng(Bitmap WindowImage, TesseractEngine Engine, string allowedChars = \"\", OcrImage.WindowType windowType = OcrImage.WindowType.Text) { string result = \"\"; Engine.SetVariable(\"tessedit_char_whitelist\", allowedChars); Bitmap rawData = WindowImage; Bitmap enhancedImage = new OcrImage(rawData).Enhance(windowType); Page page = Engine.Process(enhancedImage); using (var iter = page.GetIterator()) { iter.Begin(); do { result += iter.GetText(PageIteratorLevel.Word); } while (iter.Next(PageIteratorLevel.Word)); } page.Dispose(); return result; } /// /// Get a smaller image from a bigger one /// /// The big bitmap you want to get a part of /// The dimensions of the new bitmap /// The little bitmap protected Bitmap GetSmallBitmapFromBigOne(Bitmap inputBitmap, Rectangle newBitmapDimensions) { Bitmap sample = new Bitmap(newBitmapDimensions.Width, newBitmapDimensions.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(inputBitmap, new Rectangle(0, 0, sample.Width, sample.Height), newBitmapDimensions, GraphicsUnit.Pixel); return sample; } /// /// Returns the closest string from a list of options /// /// an array of all the possibilities /// the string you want to compare /// The closest option protected static string FindClosestMatch(List options, string testString) { var closestMatch = \"\"; var closestDistance = int.MaxValue; foreach (var item in options) { var distance = LevenshteinDistance(item, testString); if (distance < closestDistance) { closestMatch = item; closestDistance = distance; } } return closestMatch; } //This method has been generated with the help of ChatGPT /// /// Method that computes a score of distance between two strings /// /// The first string (order irrelevant) /// The second string (order irrelevant) /// The levenshtein distance protected static int LevenshteinDistance(string string1, string string2) { if (string.IsNullOrEmpty(string1)) { return string.IsNullOrEmpty(string2) ? 0 : string2.Length; } if (string.IsNullOrEmpty(string2)) { return string.IsNullOrEmpty(string1) ? 0 : string1.Length; } var d = new int[string1.Length + 1, string2.Length + 1]; for (var i = 0; i <= string1.Length; i++) { d[i, 0] = i; } for (var j = 0; j <= string2.Length; j++) { d[0, j] = j; } for (var i = 1; i <= string1.Length; i++) { for (var j = 1; j <= string2.Length; j++) { var cost = (string1[i - 1] == string2[j - 1]) ? 0 : 1; d[i, j] = Math.Min(Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost); } } return d[string1.Length, string2.Length]; } public virtual string ToJSON() { string result = \"\"; result += \"\\\"\" + Name + \"\\\"\" + \":{\" + Environment.NewLine; result += \"\\t\" + \"\\\"x\\\":\" + Bounds.X + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"y\\\":\" + Bounds.Y + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"width\\\":\" + Bounds.Width + Environment.NewLine; result += \"}\"; return result; } } }","title":"Window.cs"},{"location":"Code/DriverData.html","text":"DriverData.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverData.cs /// Brief : Class used to store Driver informations /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Test_Merge { public class DriverData { public bool DRS; //True = Drs is opened public int GapToLeader; //In ms public int LapTime; //In ms public string Name; //Ex: LECLERC public int Position; //Ex: 1 public int Sector1; //in ms public int Sector2; //in ms public int Sector3; //in ms public Tyre CurrentTyre;//Ex Soft 11 laps public DriverData(bool dRS, int gapToLeader, int lapTime, string name, int position, int sector1, int sector2, int sector3, Tyre tyre) { DRS = dRS; GapToLeader = gapToLeader; LapTime = lapTime; Name = name; Position = position; Sector1 = sector1; Sector2 = sector2; Sector3 = sector3; CurrentTyre = tyre; } public DriverData() { DRS = false; GapToLeader = -1; LapTime = -1; Name = \"Unknown\"; Position = -1; Sector1 = -1; Sector2 = -1; Sector3 = -1; CurrentTyre = new Tyre(Tyre.Type.Undefined, -1); } /// /// Method that displays all the data found in a string /// /// string containing all the driver datas public override string ToString() { string result = \"\"; //Position result += \"Position : \" + Position + Environment.NewLine; //Gap result += \"GapToLeader : \" + Reader.ConvertMsToTime(GapToLeader) + Environment.NewLine; //LapTime result += \"LapTime : \" + Reader.ConvertMsToTime(LapTime) + Environment.NewLine; //DRS result += \"DRS : \" + DRS + Environment.NewLine; //Tyres result += \"Uses \" + CurrentTyre.Coumpound + \" tyre \" + CurrentTyre.NumberOfLaps + \" laps old\" + Environment.NewLine; //Name result += \"DriverName : \" + Name + Environment.NewLine; //Sector 1 result += \"Sector1 : \" + Reader.ConvertMsToTime(Sector1) + Environment.NewLine; //Sector 1 result += \"Sector2 : \" + Reader.ConvertMsToTime(Sector2) + Environment.NewLine; //Sector 1 result += \"Sector3 : \" + Reader.ConvertMsToTime(Sector3) + Environment.NewLine; return result; } } //Structure to store tyres infos public struct Tyre { //If new tyres were to be added you will have to need to change this enum public enum Type { Soft, Medium, Hard, Inter, Wet, Undefined } public Type Coumpound; public int NumberOfLaps; public Tyre(Type type, int laps) { Coumpound = type; NumberOfLaps = laps; } } }","title":"DriverData.cs"},{"location":"Code/DriverData.html#driverdatacs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverData.cs /// Brief : Class used to store Driver informations /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Test_Merge { public class DriverData { public bool DRS; //True = Drs is opened public int GapToLeader; //In ms public int LapTime; //In ms public string Name; //Ex: LECLERC public int Position; //Ex: 1 public int Sector1; //in ms public int Sector2; //in ms public int Sector3; //in ms public Tyre CurrentTyre;//Ex Soft 11 laps public DriverData(bool dRS, int gapToLeader, int lapTime, string name, int position, int sector1, int sector2, int sector3, Tyre tyre) { DRS = dRS; GapToLeader = gapToLeader; LapTime = lapTime; Name = name; Position = position; Sector1 = sector1; Sector2 = sector2; Sector3 = sector3; CurrentTyre = tyre; } public DriverData() { DRS = false; GapToLeader = -1; LapTime = -1; Name = \"Unknown\"; Position = -1; Sector1 = -1; Sector2 = -1; Sector3 = -1; CurrentTyre = new Tyre(Tyre.Type.Undefined, -1); } /// /// Method that displays all the data found in a string /// /// string containing all the driver datas public override string ToString() { string result = \"\"; //Position result += \"Position : \" + Position + Environment.NewLine; //Gap result += \"GapToLeader : \" + Reader.ConvertMsToTime(GapToLeader) + Environment.NewLine; //LapTime result += \"LapTime : \" + Reader.ConvertMsToTime(LapTime) + Environment.NewLine; //DRS result += \"DRS : \" + DRS + Environment.NewLine; //Tyres result += \"Uses \" + CurrentTyre.Coumpound + \" tyre \" + CurrentTyre.NumberOfLaps + \" laps old\" + Environment.NewLine; //Name result += \"DriverName : \" + Name + Environment.NewLine; //Sector 1 result += \"Sector1 : \" + Reader.ConvertMsToTime(Sector1) + Environment.NewLine; //Sector 1 result += \"Sector2 : \" + Reader.ConvertMsToTime(Sector2) + Environment.NewLine; //Sector 1 result += \"Sector3 : \" + Reader.ConvertMsToTime(Sector3) + Environment.NewLine; return result; } } //Structure to store tyres infos public struct Tyre { //If new tyres were to be added you will have to need to change this enum public enum Type { Soft, Medium, Hard, Inter, Wet, Undefined } public Type Coumpound; public int NumberOfLaps; public Tyre(Type type, int laps) { Coumpound = type; NumberOfLaps = laps; } } }","title":"DriverData.cs"},{"location":"Code/DriverLapTimeWindow.html","text":"DriverLapTimeWindow.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverLapTimeWindow /// Brief : Window containing infos about the lap time of a driver /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { internal class DriverLapTimeWindow:Window { public DriverLapTimeWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"LapTime\"; } /// /// Decodes the lap time contained in the image using OCR Tesseract /// /// The laptime in int (ms) public override async Task DecodePng() { int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.LapTime, Engine); return result; } } }","title":"DriverLapTimeWindow.cs"},{"location":"Code/DriverLapTimeWindow.html#driverlaptimewindowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverLapTimeWindow /// Brief : Window containing infos about the lap time of a driver /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { internal class DriverLapTimeWindow:Window { public DriverLapTimeWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"LapTime\"; } /// /// Decodes the lap time contained in the image using OCR Tesseract /// /// The laptime in int (ms) public override async Task DecodePng() { int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.LapTime, Engine); return result; } } }","title":"DriverLapTimeWindow.cs"},{"location":"Code/DriverSectorWindow.html","text":"DriverSectorWindow.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverSectorWindow.cs /// Brief : Window containing infos about a driver sector time. Can be the first second or third, does not matter. /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { internal class DriverSectorWindow:Window { public DriverSectorWindow(Bitmap image, Rectangle bounds, int sectorId, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Sector\"+sectorId; } /// /// Decodes the sector /// /// the sector time in int (ms) public override async Task DecodePng() { int ocrResult = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector, Engine); return ocrResult; } } }","title":"DriverSectorWindow.cs"},{"location":"Code/DriverSectorWindow.html#driversectorwindowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverSectorWindow.cs /// Brief : Window containing infos about a driver sector time. Can be the first second or third, does not matter. /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { internal class DriverSectorWindow:Window { public DriverSectorWindow(Bitmap image, Rectangle bounds, int sectorId, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Sector\"+sectorId; } /// /// Decodes the sector /// /// the sector time in int (ms) public override async Task DecodePng() { int ocrResult = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector, Engine); return ocrResult; } } }","title":"DriverSectorWindow.cs"},{"location":"Code/Form1.html","text":"Form1.cs \ufeffusing System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace Test_Merge { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void btnSettings_Click(object sender, EventArgs e) { Settings settingsForm = new Settings(); settingsForm.ShowDialog(); MessageBox.Show(settingsForm.GrandPrixUrl + Environment.NewLine + settingsForm.GrandPrixName + Environment.NewLine + settingsForm.GrandPrixYear); } } }","title":"Form1.cs"},{"location":"Code/Form1.html#form1cs","text":"\ufeffusing System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace Test_Merge { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void btnSettings_Click(object sender, EventArgs e) { Settings settingsForm = new Settings(); settingsForm.ShowDialog(); MessageBox.Show(settingsForm.GrandPrixUrl + Environment.NewLine + settingsForm.GrandPrixName + Environment.NewLine + settingsForm.GrandPrixYear); } } }","title":"Form1.cs"},{"location":"Code/Reader.html","text":"Reader.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : Reader.cs /// Brief : Class used to Read the config file for the OCR /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; using System.Windows.Forms; using System.IO; using System.Text.Json; namespace Test_Merge { public class Reader { const int NUMBER_OF_DRIVERS = 20; public List Drivers; public List MainZones; public Reader(string configFile, Bitmap image,bool loadOCR = true) { MainZones = Load(image,configFile,ref Drivers,loadOCR); } /// /// Method that reads the JSON config file and create all the Zones and Windows /// /// The image #id on wich you want to create the zones on public static List Load(Bitmap image,string configFilePath,ref List driverListToFill,bool LoadOCR) { List mainZones = new List(); Bitmap fullImage = image; List drivers; Zone mainZone; try { using (var streamReader = new StreamReader(configFilePath)) { var jsonText = streamReader.ReadToEnd(); var jsonDocument = JsonDocument.Parse(jsonText); var driversNames = jsonDocument.RootElement.GetProperty(\"Drivers\"); driverListToFill = new List(); foreach (var nameElement in driversNames.EnumerateArray()) { driverListToFill.Add(nameElement.GetString()); } var mainProperty = jsonDocument.RootElement.GetProperty(\"Main\"); Point MainPosition = new Point(mainProperty.GetProperty(\"x\").GetInt32(), mainProperty.GetProperty(\"y\").GetInt32()); Size MainSize = new Size(mainProperty.GetProperty(\"width\").GetInt32(), mainProperty.GetProperty(\"height\").GetInt32()); Rectangle MainRectangle = new Rectangle(MainPosition, MainSize); mainZone = new Zone(image, MainRectangle,\"Main\"); var zones = mainProperty.GetProperty(\"Zones\"); var driverZone = zones[0].GetProperty(\"DriverZone\"); Point FirstZonePosition = new Point(driverZone.GetProperty(\"x\").GetInt32(), driverZone.GetProperty(\"y\").GetInt32()); Size FirstZoneSize = new Size(driverZone.GetProperty(\"width\").GetInt32(), driverZone.GetProperty(\"height\").GetInt32()); var windows = driverZone.GetProperty(\"Windows\"); var driverPosition = windows[0].GetProperty(\"Position\"); Size driverPositionArea = new Size(driverPosition.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverPositionPosition = new Point(driverPosition.GetProperty(\"x\").GetInt32(), driverPosition.GetProperty(\"y\").GetInt32()); var driverGapToLeader = windows[0].GetProperty(\"GapToLeader\"); Size driverGapToLeaderArea = new Size(driverGapToLeader.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverGapToLeaderPosition = new Point(driverGapToLeader.GetProperty(\"x\").GetInt32(), driverGapToLeader.GetProperty(\"y\").GetInt32()); var driverLapTime = windows[0].GetProperty(\"LapTime\"); Size driverLapTimeArea = new Size(driverLapTime.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverLapTimePosition = new Point(driverLapTime.GetProperty(\"x\").GetInt32(), driverLapTime.GetProperty(\"y\").GetInt32()); var driverDrs = windows[0].GetProperty(\"DRS\"); Size driverDrsArea = new Size(driverDrs.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverDrsPosition = new Point(driverDrs.GetProperty(\"x\").GetInt32(), driverDrs.GetProperty(\"y\").GetInt32()); var driverTyres = windows[0].GetProperty(\"Tyres\"); Size driverTyresArea = new Size(driverTyres.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverTyresPosition = new Point(driverTyres.GetProperty(\"x\").GetInt32(), driverTyres.GetProperty(\"y\").GetInt32()); var driverName = windows[0].GetProperty(\"Name\"); Size driverNameArea = new Size(driverName.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverNamePosition = new Point(driverName.GetProperty(\"x\").GetInt32(), driverName.GetProperty(\"y\").GetInt32()); var driverSector1 = windows[0].GetProperty(\"Sector1\"); Size driverSector1Area = new Size(driverSector1.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverSector1Position = new Point(driverSector1.GetProperty(\"x\").GetInt32(), driverSector1.GetProperty(\"y\").GetInt32()); var driverSector2 = windows[0].GetProperty(\"Sector2\"); Size driverSector2Area = new Size(driverSector2.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverSector2Position = new Point(driverSector2.GetProperty(\"x\").GetInt32(), driverSector2.GetProperty(\"y\").GetInt32()); var driverSector3 = windows[0].GetProperty(\"Sector3\"); Size driverSector3Area = new Size(driverSector3.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverSector3Position = new Point(driverSector3.GetProperty(\"x\").GetInt32(), driverSector3.GetProperty(\"y\").GetInt32()); float offset = (((float)mainZone.ZoneImage.Height - (float)(driverListToFill.Count * FirstZoneSize.Height)) / (float)driverListToFill.Count); Bitmap MainZoneImage = mainZone.ZoneImage; List zonesToAdd = new List(); List zonesImages = new List(); for (int i = 0; i < NUMBER_OF_DRIVERS; i++) { Point tmpPos = new Point(0, FirstZonePosition.Y + i * FirstZoneSize.Height - Convert.ToInt32(i * offset)); Zone newDriverZone = new Zone(MainZoneImage, new Rectangle(tmpPos, FirstZoneSize), \"DriverZone\"); zonesToAdd.Add(newDriverZone); zonesImages.Add(newDriverZone.ZoneImage); newDriverZone.ZoneImage.Save(\"Driver\"+i+\".png\"); } //Parallel.For(0, NUMBER_OF_DRIVERS, i => for (int i = 0; i < NUMBER_OF_DRIVERS; i++) { Zone newDriverZone = zonesToAdd[(int)i]; Bitmap zoneImg = zonesImages[(int)i]; newDriverZone.AddWindow(new DriverPositionWindow(zoneImg, new Rectangle(driverPositionPosition, driverPositionArea),LoadOCR)); newDriverZone.AddWindow(new DriverGapToLeaderWindow(zoneImg, new Rectangle(driverGapToLeaderPosition, driverGapToLeaderArea), LoadOCR)); newDriverZone.AddWindow(new DriverLapTimeWindow(zoneImg, new Rectangle(driverLapTimePosition, driverLapTimeArea), LoadOCR)); newDriverZone.AddWindow(new DriverDrsWindow(zoneImg, new Rectangle(driverDrsPosition, driverDrsArea), LoadOCR)); newDriverZone.AddWindow(new DriverTyresWindow(zoneImg, new Rectangle(driverTyresPosition, driverTyresArea), LoadOCR)); newDriverZone.AddWindow(new DriverNameWindow(zoneImg, new Rectangle(driverNamePosition, driverNameArea), LoadOCR)); newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector1Position, driverSector1Area),1, LoadOCR)); newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector2Position, driverSector2Area),2, LoadOCR)); newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector3Position, driverSector3Area),3, LoadOCR)); mainZone.AddZone(newDriverZone); }//); //MessageBox.Show(\"We have a main zone with \" + MainZone.Zones.Count() + \" Driver zones with \" + MainZone.Zones[4].Windows.Count() + \" windows each and we have \" + Drivers.Count + \" drivers\"); mainZones.Add(mainZone); } } catch (IOException ex) { MessageBox.Show(\"Error reading JSON file: \" + ex.Message); } catch (JsonException ex) { MessageBox.Show(\"Invalid JSON format: \" + ex.Message); } return mainZones; } /// /// Method that calls all the zones and windows to get the content they can find on the image to display them /// /// The id of the image we are working with /// a string representation of all the returns public async Task Decode(List mainZones,List drivers) { string result = \"\"; List mainResults = new List(); //Decode for (int mainZoneId = 0; mainZoneId < mainZones.Count; mainZoneId++) { switch (mainZoneId) { case 0: //Main Zone foreach (Zone z in mainZones[mainZoneId].Zones) { mainResults.Add(await z.Decode(Drivers)); } break; //Next there could be a Title Zone and TrackInfoZone } } //Display foreach (DriverData driver in mainResults) { result += driver.ToString(); result += Environment.NewLine; } return result; } /// /// Method that can be used to convert an amount of miliseconds into a more readable human form /// /// The given amount of miliseconds ton convert /// A human readable string that represents the ms public static string ConvertMsToTime(int amountOfMs) { //Convert.ToInt32 would round upand I dont want that int minuts = (int)((float)amountOfMs / (1000f * 60f)); int seconds = (int)((amountOfMs - (minuts * 60f * 1000f)) / 1000); int ms = amountOfMs - ((minuts * 60 * 1000) + (seconds * 1000)); return minuts + \":\" + seconds.ToString(\"00\") + \":\" + ms.ToString(\"000\"); } /// /// Old method that can draw on an image where the windows and zones are created. mostly used for debugging /// /// the #id of the image we are working with /// the drawed bitmap public Bitmap Draw(Bitmap image,List mainZones) { Graphics g = Graphics.FromImage(image); foreach (Zone z in mainZones) { int count = 0; foreach (Zone zz in z.Zones) { g.DrawRectangle(Pens.Red, z.Bounds); foreach (Window w in zz.Windows) { g.DrawRectangle(Pens.Blue, new Rectangle(z.Bounds.X + zz.Bounds.X, z.Bounds.Y + zz.Bounds.Y, zz.Bounds.Width, zz.Bounds.Height)); } count++; } } return image; } } }","title":"Reader.cs"},{"location":"Code/Reader.html#readercs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : Reader.cs /// Brief : Class used to Read the config file for the OCR /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; using System.Windows.Forms; using System.IO; using System.Text.Json; namespace Test_Merge { public class Reader { const int NUMBER_OF_DRIVERS = 20; public List Drivers; public List MainZones; public Reader(string configFile, Bitmap image,bool loadOCR = true) { MainZones = Load(image,configFile,ref Drivers,loadOCR); } /// /// Method that reads the JSON config file and create all the Zones and Windows /// /// The image #id on wich you want to create the zones on public static List Load(Bitmap image,string configFilePath,ref List driverListToFill,bool LoadOCR) { List mainZones = new List(); Bitmap fullImage = image; List drivers; Zone mainZone; try { using (var streamReader = new StreamReader(configFilePath)) { var jsonText = streamReader.ReadToEnd(); var jsonDocument = JsonDocument.Parse(jsonText); var driversNames = jsonDocument.RootElement.GetProperty(\"Drivers\"); driverListToFill = new List(); foreach (var nameElement in driversNames.EnumerateArray()) { driverListToFill.Add(nameElement.GetString()); } var mainProperty = jsonDocument.RootElement.GetProperty(\"Main\"); Point MainPosition = new Point(mainProperty.GetProperty(\"x\").GetInt32(), mainProperty.GetProperty(\"y\").GetInt32()); Size MainSize = new Size(mainProperty.GetProperty(\"width\").GetInt32(), mainProperty.GetProperty(\"height\").GetInt32()); Rectangle MainRectangle = new Rectangle(MainPosition, MainSize); mainZone = new Zone(image, MainRectangle,\"Main\"); var zones = mainProperty.GetProperty(\"Zones\"); var driverZone = zones[0].GetProperty(\"DriverZone\"); Point FirstZonePosition = new Point(driverZone.GetProperty(\"x\").GetInt32(), driverZone.GetProperty(\"y\").GetInt32()); Size FirstZoneSize = new Size(driverZone.GetProperty(\"width\").GetInt32(), driverZone.GetProperty(\"height\").GetInt32()); var windows = driverZone.GetProperty(\"Windows\"); var driverPosition = windows[0].GetProperty(\"Position\"); Size driverPositionArea = new Size(driverPosition.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverPositionPosition = new Point(driverPosition.GetProperty(\"x\").GetInt32(), driverPosition.GetProperty(\"y\").GetInt32()); var driverGapToLeader = windows[0].GetProperty(\"GapToLeader\"); Size driverGapToLeaderArea = new Size(driverGapToLeader.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverGapToLeaderPosition = new Point(driverGapToLeader.GetProperty(\"x\").GetInt32(), driverGapToLeader.GetProperty(\"y\").GetInt32()); var driverLapTime = windows[0].GetProperty(\"LapTime\"); Size driverLapTimeArea = new Size(driverLapTime.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverLapTimePosition = new Point(driverLapTime.GetProperty(\"x\").GetInt32(), driverLapTime.GetProperty(\"y\").GetInt32()); var driverDrs = windows[0].GetProperty(\"DRS\"); Size driverDrsArea = new Size(driverDrs.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverDrsPosition = new Point(driverDrs.GetProperty(\"x\").GetInt32(), driverDrs.GetProperty(\"y\").GetInt32()); var driverTyres = windows[0].GetProperty(\"Tyres\"); Size driverTyresArea = new Size(driverTyres.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverTyresPosition = new Point(driverTyres.GetProperty(\"x\").GetInt32(), driverTyres.GetProperty(\"y\").GetInt32()); var driverName = windows[0].GetProperty(\"Name\"); Size driverNameArea = new Size(driverName.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverNamePosition = new Point(driverName.GetProperty(\"x\").GetInt32(), driverName.GetProperty(\"y\").GetInt32()); var driverSector1 = windows[0].GetProperty(\"Sector1\"); Size driverSector1Area = new Size(driverSector1.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverSector1Position = new Point(driverSector1.GetProperty(\"x\").GetInt32(), driverSector1.GetProperty(\"y\").GetInt32()); var driverSector2 = windows[0].GetProperty(\"Sector2\"); Size driverSector2Area = new Size(driverSector2.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverSector2Position = new Point(driverSector2.GetProperty(\"x\").GetInt32(), driverSector2.GetProperty(\"y\").GetInt32()); var driverSector3 = windows[0].GetProperty(\"Sector3\"); Size driverSector3Area = new Size(driverSector3.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverSector3Position = new Point(driverSector3.GetProperty(\"x\").GetInt32(), driverSector3.GetProperty(\"y\").GetInt32()); float offset = (((float)mainZone.ZoneImage.Height - (float)(driverListToFill.Count * FirstZoneSize.Height)) / (float)driverListToFill.Count); Bitmap MainZoneImage = mainZone.ZoneImage; List zonesToAdd = new List(); List zonesImages = new List(); for (int i = 0; i < NUMBER_OF_DRIVERS; i++) { Point tmpPos = new Point(0, FirstZonePosition.Y + i * FirstZoneSize.Height - Convert.ToInt32(i * offset)); Zone newDriverZone = new Zone(MainZoneImage, new Rectangle(tmpPos, FirstZoneSize), \"DriverZone\"); zonesToAdd.Add(newDriverZone); zonesImages.Add(newDriverZone.ZoneImage); newDriverZone.ZoneImage.Save(\"Driver\"+i+\".png\"); } //Parallel.For(0, NUMBER_OF_DRIVERS, i => for (int i = 0; i < NUMBER_OF_DRIVERS; i++) { Zone newDriverZone = zonesToAdd[(int)i]; Bitmap zoneImg = zonesImages[(int)i]; newDriverZone.AddWindow(new DriverPositionWindow(zoneImg, new Rectangle(driverPositionPosition, driverPositionArea),LoadOCR)); newDriverZone.AddWindow(new DriverGapToLeaderWindow(zoneImg, new Rectangle(driverGapToLeaderPosition, driverGapToLeaderArea), LoadOCR)); newDriverZone.AddWindow(new DriverLapTimeWindow(zoneImg, new Rectangle(driverLapTimePosition, driverLapTimeArea), LoadOCR)); newDriverZone.AddWindow(new DriverDrsWindow(zoneImg, new Rectangle(driverDrsPosition, driverDrsArea), LoadOCR)); newDriverZone.AddWindow(new DriverTyresWindow(zoneImg, new Rectangle(driverTyresPosition, driverTyresArea), LoadOCR)); newDriverZone.AddWindow(new DriverNameWindow(zoneImg, new Rectangle(driverNamePosition, driverNameArea), LoadOCR)); newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector1Position, driverSector1Area),1, LoadOCR)); newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector2Position, driverSector2Area),2, LoadOCR)); newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector3Position, driverSector3Area),3, LoadOCR)); mainZone.AddZone(newDriverZone); }//); //MessageBox.Show(\"We have a main zone with \" + MainZone.Zones.Count() + \" Driver zones with \" + MainZone.Zones[4].Windows.Count() + \" windows each and we have \" + Drivers.Count + \" drivers\"); mainZones.Add(mainZone); } } catch (IOException ex) { MessageBox.Show(\"Error reading JSON file: \" + ex.Message); } catch (JsonException ex) { MessageBox.Show(\"Invalid JSON format: \" + ex.Message); } return mainZones; } /// /// Method that calls all the zones and windows to get the content they can find on the image to display them /// /// The id of the image we are working with /// a string representation of all the returns public async Task Decode(List mainZones,List drivers) { string result = \"\"; List mainResults = new List(); //Decode for (int mainZoneId = 0; mainZoneId < mainZones.Count; mainZoneId++) { switch (mainZoneId) { case 0: //Main Zone foreach (Zone z in mainZones[mainZoneId].Zones) { mainResults.Add(await z.Decode(Drivers)); } break; //Next there could be a Title Zone and TrackInfoZone } } //Display foreach (DriverData driver in mainResults) { result += driver.ToString(); result += Environment.NewLine; } return result; } /// /// Method that can be used to convert an amount of miliseconds into a more readable human form /// /// The given amount of miliseconds ton convert /// A human readable string that represents the ms public static string ConvertMsToTime(int amountOfMs) { //Convert.ToInt32 would round upand I dont want that int minuts = (int)((float)amountOfMs / (1000f * 60f)); int seconds = (int)((amountOfMs - (minuts * 60f * 1000f)) / 1000); int ms = amountOfMs - ((minuts * 60 * 1000) + (seconds * 1000)); return minuts + \":\" + seconds.ToString(\"00\") + \":\" + ms.ToString(\"000\"); } /// /// Old method that can draw on an image where the windows and zones are created. mostly used for debugging /// /// the #id of the image we are working with /// the drawed bitmap public Bitmap Draw(Bitmap image,List mainZones) { Graphics g = Graphics.FromImage(image); foreach (Zone z in mainZones) { int count = 0; foreach (Zone zz in z.Zones) { g.DrawRectangle(Pens.Red, z.Bounds); foreach (Window w in zz.Windows) { g.DrawRectangle(Pens.Blue, new Rectangle(z.Bounds.X + zz.Bounds.X, z.Bounds.Y + zz.Bounds.Y, zz.Bounds.Width, zz.Bounds.Height)); } count++; } } return image; } } }","title":"Reader.cs"},{"location":"Code/Zone.html","text":"Zone.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/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 : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Test_Merge { public class Zone { private Rectangle _bounds; private List _zones; private List _windows; private Bitmap _image; private string _name; public Bitmap ZoneImage { get { //This little trickery lets you have the image that the zone sees Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); return sample; } } public Bitmap Image { get { return _image; } set { //It automatically sets the image for the contained windows and zones _image = Image; foreach (Window w in Windows) { w.Image = ZoneImage; } foreach (Zone z in Zones) { z.Image = Image; } } } public Rectangle Bounds { get => _bounds; protected set => _bounds = value; } public List Zones { get => _zones; protected set => _zones = value; } public List Windows { get => _windows; protected set => _windows = value; } public string Name { get => _name; protected set => _name = value; } public Zone(Bitmap image, Rectangle bounds, string name) { Windows = new List(); Zones = new List(); Name = name; //You cant set the image in the CTOR because the processing is impossible at first initiation _image = image; Bounds = bounds; } /// /// Adds a zone to the list of zones /// /// The zone you want to add public virtual void AddZone(Zone zone) { Zones.Add(zone); } /// /// Add a window to the list of windows /// /// the window you want to add public virtual void AddWindow(Window window) { Windows.Add(window); } /// /// Calls all the windows to do OCR and to give back the results so we can send them to the model /// /// A list of all the driver in the race to help with text recognition /// A driver data object that contains all the infos about a driver public virtual async Task Decode(List driverList) { int sectorCount = 0; DriverData result = new DriverData(); Parallel.ForEach(Windows, async w => { // A switch would be prettier but I dont think its supported in this C# version if (w is DriverNameWindow) result.Name = (string)await (w as DriverNameWindow).DecodePng(driverList); if (w is DriverDrsWindow) result.DRS = (bool)await (w as DriverDrsWindow).DecodePng(); if (w is DriverGapToLeaderWindow) result.GapToLeader = (int)await (w as DriverGapToLeaderWindow).DecodePng(); if (w is DriverLapTimeWindow) result.LapTime = (int)await (w as DriverLapTimeWindow).DecodePng(); if (w is DriverPositionWindow) result.Position = (int)await (w as DriverPositionWindow).DecodePng(); if (w is DriverSectorWindow) { sectorCount++; if (sectorCount == 1) result.Sector1 = (int)await (w as DriverSectorWindow).DecodePng(); if (sectorCount == 2) result.Sector2 = (int)await (w as DriverSectorWindow).DecodePng(); if (sectorCount == 3) result.Sector3 = (int)await (w as DriverSectorWindow).DecodePng(); } if (w is DriverTyresWindow) result.CurrentTyre = (Tyre)await (w as DriverTyresWindow).DecodePng(); }); return result; } public virtual Bitmap Draw() { Bitmap img; //If its the main zone we want to see everything if (Zones.Count > 0) { img = Image; } else { img = ZoneImage; } Graphics g = Graphics.FromImage(img); //If its the main zone we need to visualize the Zone bounds displayed if (Zones.Count > 0) g.DrawRectangle(new Pen(Brushes.Violet, 5), Bounds); foreach (Zone z in Zones) { Rectangle newBounds = new Rectangle(z.Bounds.X, z.Bounds.Y + Bounds.Y, z.Bounds.Width, z.Bounds.Height); g.DrawRectangle(Pens.Red, newBounds); } foreach (Window w in Windows) { g.DrawRectangle(Pens.Blue, w.Bounds); } return img; } public void ResetZones() { Zones.Clear(); } public void ResetWindows() { foreach (Zone z in Zones) { z.ResetWindows(); } Windows.Clear(); } public virtual string ToJSON() { string result = \"\"; result += \"\\\"\" + Name + \"\\\":{\" + Environment.NewLine; result += \"\\t\" + \"\\\"x\\\":\" + Bounds.X + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"y\\\":\" + Bounds.Y + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"width\\\":\" + Bounds.Width + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"height\\\":\" + Bounds.Height; if (Windows.Count != 0) { result += \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"Windows\\\":[\" + Environment.NewLine; result += \"\\t\\t{\" + Environment.NewLine; int Wcount = 0; foreach (Window w in Windows) { result += \"\\t\\t\" + w.ToJSON(); Wcount++; if (Wcount != Windows.Count) result += \",\"; } result += \"\\t\\t}\" + Environment.NewLine; result += \"\\t\" + \"]\" + Environment.NewLine; } else { result += Environment.NewLine; } if (Zones.Count != 0) { result += \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"Zones\\\":[\" + Environment.NewLine; result += \"\\t\\t{\" + Environment.NewLine; int Zcount = 0; //foreach (Zone z in Zones) //{ result += \"\\t\\t\" + Zones[0].ToJSON(); Zcount++; if (Zcount != Zones.Count) //result += \",\"; //} result += \"\\t\\t}\" + Environment.NewLine; result += \"\\t\" + \"]\" + Environment.NewLine; } else { result += Environment.NewLine; } result += \"}\"; return result; } /// /// Checks if the given Rectangle fits in the current zone /// /// The Rectangle you want to check the fittment /// protected bool Fits(Rectangle inputRectangle) { if (inputRectangle.X + inputRectangle.Width > Bounds.Width || inputRectangle.Y + inputRectangle.Height > Bounds.Height || inputRectangle.X < 0 || inputRectangle.Y < 0) { return false; } else { return true; } } } }","title":"Zone.cs"},{"location":"Code/Zone.html#zonecs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/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 : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Test_Merge { public class Zone { private Rectangle _bounds; private List _zones; private List _windows; private Bitmap _image; private string _name; public Bitmap ZoneImage { get { //This little trickery lets you have the image that the zone sees Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); return sample; } } public Bitmap Image { get { return _image; } set { //It automatically sets the image for the contained windows and zones _image = Image; foreach (Window w in Windows) { w.Image = ZoneImage; } foreach (Zone z in Zones) { z.Image = Image; } } } public Rectangle Bounds { get => _bounds; protected set => _bounds = value; } public List Zones { get => _zones; protected set => _zones = value; } public List Windows { get => _windows; protected set => _windows = value; } public string Name { get => _name; protected set => _name = value; } public Zone(Bitmap image, Rectangle bounds, string name) { Windows = new List(); Zones = new List(); Name = name; //You cant set the image in the CTOR because the processing is impossible at first initiation _image = image; Bounds = bounds; } /// /// Adds a zone to the list of zones /// /// The zone you want to add public virtual void AddZone(Zone zone) { Zones.Add(zone); } /// /// Add a window to the list of windows /// /// the window you want to add public virtual void AddWindow(Window window) { Windows.Add(window); } /// /// Calls all the windows to do OCR and to give back the results so we can send them to the model /// /// A list of all the driver in the race to help with text recognition /// A driver data object that contains all the infos about a driver public virtual async Task Decode(List driverList) { int sectorCount = 0; DriverData result = new DriverData(); Parallel.ForEach(Windows, async w => { // A switch would be prettier but I dont think its supported in this C# version if (w is DriverNameWindow) result.Name = (string)await (w as DriverNameWindow).DecodePng(driverList); if (w is DriverDrsWindow) result.DRS = (bool)await (w as DriverDrsWindow).DecodePng(); if (w is DriverGapToLeaderWindow) result.GapToLeader = (int)await (w as DriverGapToLeaderWindow).DecodePng(); if (w is DriverLapTimeWindow) result.LapTime = (int)await (w as DriverLapTimeWindow).DecodePng(); if (w is DriverPositionWindow) result.Position = (int)await (w as DriverPositionWindow).DecodePng(); if (w is DriverSectorWindow) { sectorCount++; if (sectorCount == 1) result.Sector1 = (int)await (w as DriverSectorWindow).DecodePng(); if (sectorCount == 2) result.Sector2 = (int)await (w as DriverSectorWindow).DecodePng(); if (sectorCount == 3) result.Sector3 = (int)await (w as DriverSectorWindow).DecodePng(); } if (w is DriverTyresWindow) result.CurrentTyre = (Tyre)await (w as DriverTyresWindow).DecodePng(); }); return result; } public virtual Bitmap Draw() { Bitmap img; //If its the main zone we want to see everything if (Zones.Count > 0) { img = Image; } else { img = ZoneImage; } Graphics g = Graphics.FromImage(img); //If its the main zone we need to visualize the Zone bounds displayed if (Zones.Count > 0) g.DrawRectangle(new Pen(Brushes.Violet, 5), Bounds); foreach (Zone z in Zones) { Rectangle newBounds = new Rectangle(z.Bounds.X, z.Bounds.Y + Bounds.Y, z.Bounds.Width, z.Bounds.Height); g.DrawRectangle(Pens.Red, newBounds); } foreach (Window w in Windows) { g.DrawRectangle(Pens.Blue, w.Bounds); } return img; } public void ResetZones() { Zones.Clear(); } public void ResetWindows() { foreach (Zone z in Zones) { z.ResetWindows(); } Windows.Clear(); } public virtual string ToJSON() { string result = \"\"; result += \"\\\"\" + Name + \"\\\":{\" + Environment.NewLine; result += \"\\t\" + \"\\\"x\\\":\" + Bounds.X + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"y\\\":\" + Bounds.Y + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"width\\\":\" + Bounds.Width + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"height\\\":\" + Bounds.Height; if (Windows.Count != 0) { result += \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"Windows\\\":[\" + Environment.NewLine; result += \"\\t\\t{\" + Environment.NewLine; int Wcount = 0; foreach (Window w in Windows) { result += \"\\t\\t\" + w.ToJSON(); Wcount++; if (Wcount != Windows.Count) result += \",\"; } result += \"\\t\\t}\" + Environment.NewLine; result += \"\\t\" + \"]\" + Environment.NewLine; } else { result += Environment.NewLine; } if (Zones.Count != 0) { result += \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"Zones\\\":[\" + Environment.NewLine; result += \"\\t\\t{\" + Environment.NewLine; int Zcount = 0; //foreach (Zone z in Zones) //{ result += \"\\t\\t\" + Zones[0].ToJSON(); Zcount++; if (Zcount != Zones.Count) //result += \",\"; //} result += \"\\t\\t}\" + Environment.NewLine; result += \"\\t\" + \"]\" + Environment.NewLine; } else { result += Environment.NewLine; } result += \"}\"; return result; } /// /// Checks if the given Rectangle fits in the current zone /// /// The Rectangle you want to check the fittment /// protected bool Fits(Rectangle inputRectangle) { if (inputRectangle.X + inputRectangle.Width > Bounds.Width || inputRectangle.Y + inputRectangle.Height > Bounds.Height || inputRectangle.X < 0 || inputRectangle.Y < 0) { return false; } else { return true; } } } }","title":"Zone.cs"},{"location":"Code/DriverDrsWindow.html","text":"DriverDrsWindow.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverDrsWindow.cs /// Brief : Window containing DRS related method and infos /// Version : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.Linq; using System.Text; using System.Threading.Tasks; using Tesseract; namespace Test_Merge { internal class DriverDrsWindow:Window { private static int EmptyDrsGreenValue = -1; private static Random rnd = new Random(); public DriverDrsWindow(Bitmap image, Rectangle bounds,bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"DRS\"; } public override async Task DecodePng() { bool result = false; int greenValue = GetGreenPixels(); if (EmptyDrsGreenValue == -1) EmptyDrsGreenValue = greenValue; if (greenValue > EmptyDrsGreenValue + EmptyDrsGreenValue / 100 * 30) result = true; return result; } private unsafe int GetGreenPixels() { int tot = 0; Bitmap bmp = WindowImage; Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height); BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, bmp.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); for (int y = 0; y < bmp.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < bmp.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); byte blue = pixel[0]; byte green = pixel[1]; byte red = pixel[2]; if (green > blue * 1.5 && green > red * 1.5) { tot++; } } } } bmp.UnlockBits(bmpData); return tot; } public Rectangle GetBox() { var tessImage = Pix.LoadFromMemory(ImageToByte(WindowImage)); Engine.SetVariable(\"tessedit_char_whitelist\", \"\"); Page page = Engine.Process(tessImage); using (var iter = page.GetIterator()) { iter.Begin(); do { Rect boundingBox; // Get the bounding box for the current element if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox)) { page.Dispose(); return new Rectangle(boundingBox.X1, boundingBox.X2, boundingBox.Width, boundingBox.Height); } } while (iter.Next(PageIteratorLevel.Word)); page.Dispose(); return new Rectangle(0, 0, 0, 0); } } } }","title":"DriverDrsWindow.cs"},{"location":"Code/DriverDrsWindow.html#driverdrswindowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverDrsWindow.cs /// Brief : Window containing DRS related method and infos /// Version : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.Linq; using System.Text; using System.Threading.Tasks; using Tesseract; namespace Test_Merge { internal class DriverDrsWindow:Window { private static int EmptyDrsGreenValue = -1; private static Random rnd = new Random(); public DriverDrsWindow(Bitmap image, Rectangle bounds,bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"DRS\"; } public override async Task DecodePng() { bool result = false; int greenValue = GetGreenPixels(); if (EmptyDrsGreenValue == -1) EmptyDrsGreenValue = greenValue; if (greenValue > EmptyDrsGreenValue + EmptyDrsGreenValue / 100 * 30) result = true; return result; } private unsafe int GetGreenPixels() { int tot = 0; Bitmap bmp = WindowImage; Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height); BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, bmp.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); for (int y = 0; y < bmp.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < bmp.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); byte blue = pixel[0]; byte green = pixel[1]; byte red = pixel[2]; if (green > blue * 1.5 && green > red * 1.5) { tot++; } } } } bmp.UnlockBits(bmpData); return tot; } public Rectangle GetBox() { var tessImage = Pix.LoadFromMemory(ImageToByte(WindowImage)); Engine.SetVariable(\"tessedit_char_whitelist\", \"\"); Page page = Engine.Process(tessImage); using (var iter = page.GetIterator()) { iter.Begin(); do { Rect boundingBox; // Get the bounding box for the current element if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox)) { page.Dispose(); return new Rectangle(boundingBox.X1, boundingBox.X2, boundingBox.Width, boundingBox.Height); } } while (iter.Next(PageIteratorLevel.Word)); page.Dispose(); return new Rectangle(0, 0, 0, 0); } } } }","title":"DriverDrsWindow.cs"},{"location":"Code/DriverNameWindow.html","text":"DriverNameWindow.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverNameWindow /// Brief : Window containing infos about the name of the driver /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { public class DriverNameWindow : Window { public static Random rnd = new Random(); public DriverNameWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Name\"; } /// /// Decodes using OCR wich driver name is in the image /// /// /// The driver name in string public override async Task DecodePng(List DriverList) { string result = \"\"; result = await GetStringFromPng(WindowImage, Engine); if (!IsADriver(DriverList, result)) { //I put everything in uppercase to try to lower the chances of bad answers result = FindClosestMatch(DriverList.ConvertAll(d => d.ToUpper()), result.ToUpper()); } return result; } /// /// Verifies that the name found in the OCR is a valid name /// /// /// /// If ye or no the driver exists private static bool IsADriver(List driverList, string potentialDriver) { bool result = false; //I cant use drivers.Contains because it has missmatched cases and all foreach (string name in driverList) { if (name.ToUpper() == potentialDriver.ToUpper()) result = true; } return result; } } }","title":"DriverNameWindow.cs"},{"location":"Code/DriverNameWindow.html#drivernamewindowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverNameWindow /// Brief : Window containing infos about the name of the driver /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { public class DriverNameWindow : Window { public static Random rnd = new Random(); public DriverNameWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Name\"; } /// /// Decodes using OCR wich driver name is in the image /// /// /// The driver name in string public override async Task DecodePng(List DriverList) { string result = \"\"; result = await GetStringFromPng(WindowImage, Engine); if (!IsADriver(DriverList, result)) { //I put everything in uppercase to try to lower the chances of bad answers result = FindClosestMatch(DriverList.ConvertAll(d => d.ToUpper()), result.ToUpper()); } return result; } /// /// Verifies that the name found in the OCR is a valid name /// /// /// /// If ye or no the driver exists private static bool IsADriver(List driverList, string potentialDriver) { bool result = false; //I cant use drivers.Contains because it has missmatched cases and all foreach (string name in driverList) { if (name.ToUpper() == potentialDriver.ToUpper()) result = true; } return result; } } }","title":"DriverNameWindow.cs"},{"location":"Code/DriverTyresWindow.html","text":"DriverTyresWindow.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverTyresWindow.cs /// Brief : Window containing infos about a driver's tyre /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { public class DriverTyresWindow:Window { private static Random rnd = new Random(); int seed = rnd.Next(0, 10000); //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); public static Color HARD_TYRE_COLOR = Color.FromArgb(0xa4, 0xa5, 0xa8); public static Color INTER_TYRE_COLOR = Color.FromArgb(0x00, 0xa4, 0x2e); public static Color WET_TYRE_COLOR = Color.FromArgb(0x27, 0x60, 0xa6); public static Color EMPTY_COLOR = Color.FromArgb(0x20, 0x20, 0x20); public DriverTyresWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Tyres\"; } /// /// This will decode the content of the image /// /// And object containing what was on the image public override async Task DecodePng() { return await GetTyreInfos(); } /// /// Method that will decode whats on the image and return the tyre infos it could manage to recover /// /// A tyre object containing tyre infos private async Task GetTyreInfos() { Bitmap tyreZone = GetSmallBitmapFromBigOne(WindowImage, FindTyreZone()); Tyre.Type type = Tyre.Type.Undefined; type = GetTyreTypeFromColor(OcrImage.GetAvgColorFromBitmap(tyreZone)); int laps = -1; string number = await GetStringFromPng(tyreZone, Engine, \"0123456789\", OcrImage.WindowType.Tyre); try { laps = Convert.ToInt32(number); } catch { //We could not convert the number so its a letter so its 0 laps old laps = 0; } //tyreZone.Save(Reader.DEBUG_DUMP_FOLDER + \"Tyre\" + type + \"Laps\" + laps + '#' + rnd.Next(0, 1000) + \".png\"); return new Tyre(type, laps); } /// /// Finds where the important part of the image is /// /// A rectangle containing position and dimensions of the important part of the image private Rectangle FindTyreZone() { Bitmap bmp = WindowImage; int currentPosition = bmp.Width; int height = bmp.Height / 2; Color limitColor = Color.FromArgb(0x50, 0x50, 0x50); Color currentColor = Color.FromArgb(0, 0, 0); Size newWindowSize = new Size(bmp.Height - Convert.ToInt32((float)bmp.Height / 100f * 25f), bmp.Height - Convert.ToInt32((float)bmp.Height / 100f * 35f)); while (currentColor.R <= limitColor.R && currentColor.G <= limitColor.G && currentColor.B <= limitColor.B && currentPosition > 0) { currentPosition--; currentColor = bmp.GetPixel(currentPosition, height); } //Its here to let the new window include a little bit of the right int CorrectedX = currentPosition - (newWindowSize.Width) + Convert.ToInt32((float)newWindowSize.Width / 100f * 10f); int CorrectedY = Convert.ToInt32((float)newWindowSize.Height / 100f * 35f); if (CorrectedX <= 0) return new Rectangle(0, 0, newWindowSize.Width, newWindowSize.Height); return new Rectangle(CorrectedX, CorrectedY, newWindowSize.Width, newWindowSize.Height); } //This method has been created with the help of chatGPT /// /// Methods that compares a list of colors to see wich is the closest from the input color and decide wich tyre type it is /// /// The color that you found /// The tyre type public Tyre.Type GetTyreTypeFromColor(Color inputColor) { Tyre.Type type = Tyre.Type.Undefined; List colors = new List(); //dont forget that if for some reason someday F1 adds a new Tyre type you will need to add it in the constants but also here in the list //You will also need to add it below in the Tyre object's enum and add an if in the end of this method colors.Add(SOFT_TYRE_COLOR); colors.Add(MEDIUM_TYRE_COLOR); colors.Add(HARD_TYRE_COLOR); colors.Add(INTER_TYRE_COLOR); colors.Add(WET_TYRE_COLOR); colors.Add(EMPTY_COLOR); Color closestColor = colors[0]; int closestDistance = int.MaxValue; foreach (Color color in colors) { int distance = Math.Abs(color.R - inputColor.R) + Math.Abs(color.G - inputColor.G) + Math.Abs(color.B - inputColor.B); if (distance < closestDistance) { closestColor = color; closestDistance = distance; } } //We cant use a switch as the colors cant be constants ... if (closestColor == SOFT_TYRE_COLOR) type = Tyre.Type.Soft; if (closestColor == MEDIUM_TYRE_COLOR) type = Tyre.Type.Medium; if (closestColor == HARD_TYRE_COLOR) type = Tyre.Type.Hard; if (closestColor == INTER_TYRE_COLOR) type = Tyre.Type.Inter; if (closestColor == WET_TYRE_COLOR) type = Tyre.Type.Wet; if (closestColor == EMPTY_COLOR) return Tyre.Type.Undefined; return type; } } }","title":"DriverTyresWindow.cs"},{"location":"Code/DriverTyresWindow.html#drivertyreswindowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverTyresWindow.cs /// Brief : Window containing infos about a driver's tyre /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { public class DriverTyresWindow:Window { private static Random rnd = new Random(); int seed = rnd.Next(0, 10000); //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); public static Color HARD_TYRE_COLOR = Color.FromArgb(0xa4, 0xa5, 0xa8); public static Color INTER_TYRE_COLOR = Color.FromArgb(0x00, 0xa4, 0x2e); public static Color WET_TYRE_COLOR = Color.FromArgb(0x27, 0x60, 0xa6); public static Color EMPTY_COLOR = Color.FromArgb(0x20, 0x20, 0x20); public DriverTyresWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Tyres\"; } /// /// This will decode the content of the image /// /// And object containing what was on the image public override async Task DecodePng() { return await GetTyreInfos(); } /// /// Method that will decode whats on the image and return the tyre infos it could manage to recover /// /// A tyre object containing tyre infos private async Task GetTyreInfos() { Bitmap tyreZone = GetSmallBitmapFromBigOne(WindowImage, FindTyreZone()); Tyre.Type type = Tyre.Type.Undefined; type = GetTyreTypeFromColor(OcrImage.GetAvgColorFromBitmap(tyreZone)); int laps = -1; string number = await GetStringFromPng(tyreZone, Engine, \"0123456789\", OcrImage.WindowType.Tyre); try { laps = Convert.ToInt32(number); } catch { //We could not convert the number so its a letter so its 0 laps old laps = 0; } //tyreZone.Save(Reader.DEBUG_DUMP_FOLDER + \"Tyre\" + type + \"Laps\" + laps + '#' + rnd.Next(0, 1000) + \".png\"); return new Tyre(type, laps); } /// /// Finds where the important part of the image is /// /// A rectangle containing position and dimensions of the important part of the image private Rectangle FindTyreZone() { Bitmap bmp = WindowImage; int currentPosition = bmp.Width; int height = bmp.Height / 2; Color limitColor = Color.FromArgb(0x50, 0x50, 0x50); Color currentColor = Color.FromArgb(0, 0, 0); Size newWindowSize = new Size(bmp.Height - Convert.ToInt32((float)bmp.Height / 100f * 25f), bmp.Height - Convert.ToInt32((float)bmp.Height / 100f * 35f)); while (currentColor.R <= limitColor.R && currentColor.G <= limitColor.G && currentColor.B <= limitColor.B && currentPosition > 0) { currentPosition--; currentColor = bmp.GetPixel(currentPosition, height); } //Its here to let the new window include a little bit of the right int CorrectedX = currentPosition - (newWindowSize.Width) + Convert.ToInt32((float)newWindowSize.Width / 100f * 10f); int CorrectedY = Convert.ToInt32((float)newWindowSize.Height / 100f * 35f); if (CorrectedX <= 0) return new Rectangle(0, 0, newWindowSize.Width, newWindowSize.Height); return new Rectangle(CorrectedX, CorrectedY, newWindowSize.Width, newWindowSize.Height); } //This method has been created with the help of chatGPT /// /// Methods that compares a list of colors to see wich is the closest from the input color and decide wich tyre type it is /// /// The color that you found /// The tyre type public Tyre.Type GetTyreTypeFromColor(Color inputColor) { Tyre.Type type = Tyre.Type.Undefined; List colors = new List(); //dont forget that if for some reason someday F1 adds a new Tyre type you will need to add it in the constants but also here in the list //You will also need to add it below in the Tyre object's enum and add an if in the end of this method colors.Add(SOFT_TYRE_COLOR); colors.Add(MEDIUM_TYRE_COLOR); colors.Add(HARD_TYRE_COLOR); colors.Add(INTER_TYRE_COLOR); colors.Add(WET_TYRE_COLOR); colors.Add(EMPTY_COLOR); Color closestColor = colors[0]; int closestDistance = int.MaxValue; foreach (Color color in colors) { int distance = Math.Abs(color.R - inputColor.R) + Math.Abs(color.G - inputColor.G) + Math.Abs(color.B - inputColor.B); if (distance < closestDistance) { closestColor = color; closestDistance = distance; } } //We cant use a switch as the colors cant be constants ... if (closestColor == SOFT_TYRE_COLOR) type = Tyre.Type.Soft; if (closestColor == MEDIUM_TYRE_COLOR) type = Tyre.Type.Medium; if (closestColor == HARD_TYRE_COLOR) type = Tyre.Type.Hard; if (closestColor == INTER_TYRE_COLOR) type = Tyre.Type.Inter; if (closestColor == WET_TYRE_COLOR) type = Tyre.Type.Wet; if (closestColor == EMPTY_COLOR) return Tyre.Type.Undefined; return type; } } }","title":"DriverTyresWindow.cs"},{"location":"Code/OcrImage.html","text":"OcrImage.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : OcrImage.cs /// Brief : Class containing all the methods used to enhance images for OCR /// Version : 0.1 using System; using System.Collections.Generic; using System.Threading.Tasks; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; namespace Test_Merge { public class OcrImage { //this is a hardcoded value based on the colors of the F1TV data channel background you can change it if sometime in the future the color changes //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; public enum WindowType { LapTime, Text, Sector, Gap, Tyre, } /// /// Create a new Ocr image to help enhance the given bitmap for OCR /// /// The image you want to enhance public OcrImage(Bitmap inputBitmap) { InputBitmap = inputBitmap; } /// /// Enhances the image depending on wich type of window the image comes from /// /// The type of the window. Depending on it different enhancing features will be applied /// The enhanced Bitmap public Bitmap Enhance(WindowType type = WindowType.Text) { Bitmap outputBitmap = (Bitmap)InputBitmap.Clone(); switch (type) { case WindowType.LapTime: outputBitmap = Tresholding(outputBitmap, 185); outputBitmap = Resize(outputBitmap, 2); outputBitmap = Dilatation(outputBitmap, 1); outputBitmap = Erode(outputBitmap, 1); break; case WindowType.Text: outputBitmap = InvertColors(outputBitmap); outputBitmap = Tresholding(outputBitmap, 165); outputBitmap = Resize(outputBitmap, 2); outputBitmap = Dilatation(outputBitmap, 1); break; case WindowType.Tyre: outputBitmap = RemoveUseless(outputBitmap); outputBitmap = Resize(outputBitmap, 4); outputBitmap = Dilatation(outputBitmap, 1); break; default: outputBitmap = Tresholding(outputBitmap, 165); outputBitmap = Resize(outputBitmap, 4); outputBitmap = Erode(outputBitmap, 1); break; } return outputBitmap; } /// /// Method that convert a colored RGB bitmap into a GrayScale image /// /// The Bitmap you want to convert /// The bitmap in grayscale 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); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8; unsafe { 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); byte blue = pixel[0]; byte green = pixel[1]; byte red = pixel[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); pixel[0] = pixel[1] = pixel[2] = (byte)gray; } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// /// Method that binaries the input image up to a certain treshold given /// /// the bitmap you want to convert to binary colors /// The floor at wich the color is considered as white or black /// The binarised bitmap public static Bitmap Tresholding(Bitmap inputBitmap, int threshold) { 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 { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); int bmpHeight = inputBitmap.Height; int bmpWidth = inputBitmap.Width; Parallel.For(0, bmpHeight, y => { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < bmpWidth; x++) { byte* pixel = currentLine + (x * bytesPerPixel); byte blue = pixel[0]; byte green = pixel[1]; byte red = pixel[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); int value = gray < threshold ? 0 : 255; pixel[0] = pixel[1] = pixel[2] = (byte)value; } }); } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// /// Method that removes the pixels that are flagged as background /// /// The bitmap you want to remove the background from /// The Bitmap without the background public static Bitmap RemoveBG(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 { 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); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; 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); return inputBitmap; } /// /// Method that removes all the useless things from the image and returns hopefully only the numbers /// /// The bitmap you want to remove useless things from (Expects a cropped part of the TyreWindow) /// The bitmap with (hopefully) only the digits public unsafe static Bitmap RemoveUseless(Bitmap inputBitmap) { //Note you can use something else than a cropped tyre window but I would recommend checking the code first to see if it fits your intended use 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; byte* ptr = (byte*)bmpData.Scan0.ToPointer(); for (int y = 0; y < inputBitmap.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); List pixelsToRemove = new List(); bool fromBorder = true; for (int x = 0; x < inputBitmap.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (fromBorder && B < F1TV_BACKGROUND_TRESHOLD.B && G < F1TV_BACKGROUND_TRESHOLD.G && R < F1TV_BACKGROUND_TRESHOLD.R) { pixelsToRemove.Add(x); } else { if (fromBorder) { fromBorder = false; pixelsToRemove.Add(x); } } } fromBorder = true; for (int x = inputBitmap.Width - 1; x > 0; x--) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (fromBorder && B < F1TV_BACKGROUND_TRESHOLD.B && G < F1TV_BACKGROUND_TRESHOLD.G && R < F1TV_BACKGROUND_TRESHOLD.R) { pixelsToRemove.Add(x); } else { if (fromBorder) { fromBorder = false; pixelsToRemove.Add(x); } } } foreach (int pxPos in pixelsToRemove) { byte* pixel = currentLine + (pxPos * bytesPerPixel); pixel[0] = 0xFF; pixel[1] = 0xFF; pixel[2] = 0xFF; } } //Removing the color parts 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); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (R >= F1TV_BACKGROUND_TRESHOLD.R + 15 || G >= F1TV_BACKGROUND_TRESHOLD.G + 15 || B >= F1TV_BACKGROUND_TRESHOLD.B + 15) { pixel[0] = 0xFF; pixel[1] = 0xFF; pixel[2] = 0xFF; } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// /// Recovers the average colors from the Image. NOTE : It wont take in account colors that are lower than the background /// /// The bitmap you want to get the average color from /// The average color of the bitmap public static Color GetAvgColorFromBitmap(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; int totR = 0; int totG = 0; int totB = 0; int totPixels = 1; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); int bmpHeight = inputBitmap.Height; int bmpWidth = inputBitmap.Width; Parallel.For(0, bmpHeight, y => { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < bmpWidth; x++) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (R >= F1TV_BACKGROUND_TRESHOLD.R || G >= F1TV_BACKGROUND_TRESHOLD.G || B >= F1TV_BACKGROUND_TRESHOLD.B) { totPixels++; totB += pixel[0]; totG += pixel[1]; totR += pixel[2]; } } }); } inputBitmap.UnlockBits(bmpData); return Color.FromArgb(255, Convert.ToInt32((float)totR / (float)totPixels), Convert.ToInt32((float)totG / (float)totPixels), Convert.ToInt32((float)totB / (float)totPixels)); } /// /// This method simply inverts all the colors in a Bitmap /// /// the bitmap you want to invert the colors from /// The bitmap with inverted colors public static Bitmap InvertColors(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 { 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); pixel[0] = (byte)(255 - pixel[0]); pixel[1] = (byte)(255 - pixel[1]); pixel[2] = (byte)(255 - pixel[2]); } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// /// Methods that applies Bicubic interpolation to increase the size and resolution of an image /// /// The bitmap you want to resize /// The factor of resizing you want to use. I recommend using even numbers /// The bitmap witht the new size public static Bitmap Resize(Bitmap inputBitmap, int resizeFactor) { var resultBitmap = new Bitmap(inputBitmap.Width * resizeFactor, inputBitmap.Height * resizeFactor); using (var graphics = Graphics.FromImage(resultBitmap)) { graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.DrawImage(inputBitmap, new Rectangle(0, 0, resultBitmap.Width, resultBitmap.Height)); } return resultBitmap; } /// /// method that Highlights the countours of a Bitmap /// /// The bitmap you want to highlight the countours of /// The bitmap with countours highlighted public static Bitmap HighlightContours(Bitmap inputBitmap) { Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); Bitmap grayscale = Grayscale(inputBitmap); Bitmap thresholded = Tresholding(grayscale, 128); Bitmap dilated = Dilatation(thresholded, 3); Bitmap eroded = Erode(dilated, 3); for (int y = 0; y < inputBitmap.Height; y++) { for (int x = 0; x < inputBitmap.Width; x++) { Color pixel = inputBitmap.GetPixel(x, y); Color dilatedPixel = dilated.GetPixel(x, y); Color erodedPixel = eroded.GetPixel(x, y); int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); int threshold = dilatedPixel.R; if (gray > threshold) { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); } else if (gray <= threshold && erodedPixel.R == 0) { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 0, 0)); } else { outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); } } } return outputBitmap; } /// /// Method that that erodes the morphology of a bitmap /// /// The bitmap you want to erode /// The amount of Erosion you want (be carefull its expensive on ressources) /// The Bitmap with the eroded contents public static Bitmap Erode(Bitmap inputBitmap, int kernelSize) { Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); int[,] kernel = new int[kernelSize, kernelSize]; for (int i = 0; i < kernelSize; i++) { for (int j = 0; j < kernelSize; j++) { kernel[i, j] = 1; } } for (int y = kernelSize / 2; y < inputBitmap.Height - kernelSize / 2; y++) { for (int x = kernelSize / 2; x < inputBitmap.Width - kernelSize / 2; x++) { bool flag = true; for (int i = -kernelSize / 2; i <= kernelSize / 2; i++) { for (int j = -kernelSize / 2; j <= kernelSize / 2; j++) { Color pixel = inputBitmap.GetPixel(x + i, y + j); int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); if (gray >= 128 && kernel[i + kernelSize / 2, j + kernelSize / 2] == 1) { flag = false; break; } } if (!flag) { break; } } if (flag) { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); } else { outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); } } } return outputBitmap; } /// /// Method that that use dilatation of the morphology of a bitmap /// /// The bitmap you want to use dilatation on /// The amount of dilatation you want (be carefull its expensive on ressources) /// The Bitmap after Dilatation public static Bitmap Dilatation(Bitmap inputBitmap, int kernelSize) { Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); int[,] kernel = new int[kernelSize, kernelSize]; for (int i = 0; i < kernelSize; i++) { for (int j = 0; j < kernelSize; j++) { kernel[i, j] = 1; } } for (int y = kernelSize / 2; y < inputBitmap.Height - kernelSize / 2; y++) { for (int x = kernelSize / 2; x < inputBitmap.Width - kernelSize / 2; x++) { bool flag = false; for (int i = -kernelSize / 2; i <= kernelSize / 2; i++) { for (int j = -kernelSize / 2; j <= kernelSize / 2; j++) { Color pixel = inputBitmap.GetPixel(x + i, y + j); int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); if (gray < 128 && kernel[i + kernelSize / 2, j + kernelSize / 2] == 1) { flag = true; break; } } if (flag) { break; } } if (flag) { outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); } else { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); } } } return outputBitmap; } } }","title":"OcrImage.cs"},{"location":"Code/OcrImage.html#ocrimagecs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : OcrImage.cs /// Brief : Class containing all the methods used to enhance images for OCR /// Version : 0.1 using System; using System.Collections.Generic; using System.Threading.Tasks; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; namespace Test_Merge { public class OcrImage { //this is a hardcoded value based on the colors of the F1TV data channel background you can change it if sometime in the future the color changes //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; public enum WindowType { LapTime, Text, Sector, Gap, Tyre, } /// /// Create a new Ocr image to help enhance the given bitmap for OCR /// /// The image you want to enhance public OcrImage(Bitmap inputBitmap) { InputBitmap = inputBitmap; } /// /// Enhances the image depending on wich type of window the image comes from /// /// The type of the window. Depending on it different enhancing features will be applied /// The enhanced Bitmap public Bitmap Enhance(WindowType type = WindowType.Text) { Bitmap outputBitmap = (Bitmap)InputBitmap.Clone(); switch (type) { case WindowType.LapTime: outputBitmap = Tresholding(outputBitmap, 185); outputBitmap = Resize(outputBitmap, 2); outputBitmap = Dilatation(outputBitmap, 1); outputBitmap = Erode(outputBitmap, 1); break; case WindowType.Text: outputBitmap = InvertColors(outputBitmap); outputBitmap = Tresholding(outputBitmap, 165); outputBitmap = Resize(outputBitmap, 2); outputBitmap = Dilatation(outputBitmap, 1); break; case WindowType.Tyre: outputBitmap = RemoveUseless(outputBitmap); outputBitmap = Resize(outputBitmap, 4); outputBitmap = Dilatation(outputBitmap, 1); break; default: outputBitmap = Tresholding(outputBitmap, 165); outputBitmap = Resize(outputBitmap, 4); outputBitmap = Erode(outputBitmap, 1); break; } return outputBitmap; } /// /// Method that convert a colored RGB bitmap into a GrayScale image /// /// The Bitmap you want to convert /// The bitmap in grayscale 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); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8; unsafe { 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); byte blue = pixel[0]; byte green = pixel[1]; byte red = pixel[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); pixel[0] = pixel[1] = pixel[2] = (byte)gray; } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// /// Method that binaries the input image up to a certain treshold given /// /// the bitmap you want to convert to binary colors /// The floor at wich the color is considered as white or black /// The binarised bitmap public static Bitmap Tresholding(Bitmap inputBitmap, int threshold) { 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 { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); int bmpHeight = inputBitmap.Height; int bmpWidth = inputBitmap.Width; Parallel.For(0, bmpHeight, y => { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < bmpWidth; x++) { byte* pixel = currentLine + (x * bytesPerPixel); byte blue = pixel[0]; byte green = pixel[1]; byte red = pixel[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); int value = gray < threshold ? 0 : 255; pixel[0] = pixel[1] = pixel[2] = (byte)value; } }); } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// /// Method that removes the pixels that are flagged as background /// /// The bitmap you want to remove the background from /// The Bitmap without the background public static Bitmap RemoveBG(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 { 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); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; 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); return inputBitmap; } /// /// Method that removes all the useless things from the image and returns hopefully only the numbers /// /// The bitmap you want to remove useless things from (Expects a cropped part of the TyreWindow) /// The bitmap with (hopefully) only the digits public unsafe static Bitmap RemoveUseless(Bitmap inputBitmap) { //Note you can use something else than a cropped tyre window but I would recommend checking the code first to see if it fits your intended use 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; byte* ptr = (byte*)bmpData.Scan0.ToPointer(); for (int y = 0; y < inputBitmap.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); List pixelsToRemove = new List(); bool fromBorder = true; for (int x = 0; x < inputBitmap.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (fromBorder && B < F1TV_BACKGROUND_TRESHOLD.B && G < F1TV_BACKGROUND_TRESHOLD.G && R < F1TV_BACKGROUND_TRESHOLD.R) { pixelsToRemove.Add(x); } else { if (fromBorder) { fromBorder = false; pixelsToRemove.Add(x); } } } fromBorder = true; for (int x = inputBitmap.Width - 1; x > 0; x--) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (fromBorder && B < F1TV_BACKGROUND_TRESHOLD.B && G < F1TV_BACKGROUND_TRESHOLD.G && R < F1TV_BACKGROUND_TRESHOLD.R) { pixelsToRemove.Add(x); } else { if (fromBorder) { fromBorder = false; pixelsToRemove.Add(x); } } } foreach (int pxPos in pixelsToRemove) { byte* pixel = currentLine + (pxPos * bytesPerPixel); pixel[0] = 0xFF; pixel[1] = 0xFF; pixel[2] = 0xFF; } } //Removing the color parts 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); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (R >= F1TV_BACKGROUND_TRESHOLD.R + 15 || G >= F1TV_BACKGROUND_TRESHOLD.G + 15 || B >= F1TV_BACKGROUND_TRESHOLD.B + 15) { pixel[0] = 0xFF; pixel[1] = 0xFF; pixel[2] = 0xFF; } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// /// Recovers the average colors from the Image. NOTE : It wont take in account colors that are lower than the background /// /// The bitmap you want to get the average color from /// The average color of the bitmap public static Color GetAvgColorFromBitmap(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; int totR = 0; int totG = 0; int totB = 0; int totPixels = 1; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); int bmpHeight = inputBitmap.Height; int bmpWidth = inputBitmap.Width; Parallel.For(0, bmpHeight, y => { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < bmpWidth; x++) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (R >= F1TV_BACKGROUND_TRESHOLD.R || G >= F1TV_BACKGROUND_TRESHOLD.G || B >= F1TV_BACKGROUND_TRESHOLD.B) { totPixels++; totB += pixel[0]; totG += pixel[1]; totR += pixel[2]; } } }); } inputBitmap.UnlockBits(bmpData); return Color.FromArgb(255, Convert.ToInt32((float)totR / (float)totPixels), Convert.ToInt32((float)totG / (float)totPixels), Convert.ToInt32((float)totB / (float)totPixels)); } /// /// This method simply inverts all the colors in a Bitmap /// /// the bitmap you want to invert the colors from /// The bitmap with inverted colors public static Bitmap InvertColors(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 { 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); pixel[0] = (byte)(255 - pixel[0]); pixel[1] = (byte)(255 - pixel[1]); pixel[2] = (byte)(255 - pixel[2]); } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// /// Methods that applies Bicubic interpolation to increase the size and resolution of an image /// /// The bitmap you want to resize /// The factor of resizing you want to use. I recommend using even numbers /// The bitmap witht the new size public static Bitmap Resize(Bitmap inputBitmap, int resizeFactor) { var resultBitmap = new Bitmap(inputBitmap.Width * resizeFactor, inputBitmap.Height * resizeFactor); using (var graphics = Graphics.FromImage(resultBitmap)) { graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.DrawImage(inputBitmap, new Rectangle(0, 0, resultBitmap.Width, resultBitmap.Height)); } return resultBitmap; } /// /// method that Highlights the countours of a Bitmap /// /// The bitmap you want to highlight the countours of /// The bitmap with countours highlighted public static Bitmap HighlightContours(Bitmap inputBitmap) { Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); Bitmap grayscale = Grayscale(inputBitmap); Bitmap thresholded = Tresholding(grayscale, 128); Bitmap dilated = Dilatation(thresholded, 3); Bitmap eroded = Erode(dilated, 3); for (int y = 0; y < inputBitmap.Height; y++) { for (int x = 0; x < inputBitmap.Width; x++) { Color pixel = inputBitmap.GetPixel(x, y); Color dilatedPixel = dilated.GetPixel(x, y); Color erodedPixel = eroded.GetPixel(x, y); int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); int threshold = dilatedPixel.R; if (gray > threshold) { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); } else if (gray <= threshold && erodedPixel.R == 0) { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 0, 0)); } else { outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); } } } return outputBitmap; } /// /// Method that that erodes the morphology of a bitmap /// /// The bitmap you want to erode /// The amount of Erosion you want (be carefull its expensive on ressources) /// The Bitmap with the eroded contents public static Bitmap Erode(Bitmap inputBitmap, int kernelSize) { Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); int[,] kernel = new int[kernelSize, kernelSize]; for (int i = 0; i < kernelSize; i++) { for (int j = 0; j < kernelSize; j++) { kernel[i, j] = 1; } } for (int y = kernelSize / 2; y < inputBitmap.Height - kernelSize / 2; y++) { for (int x = kernelSize / 2; x < inputBitmap.Width - kernelSize / 2; x++) { bool flag = true; for (int i = -kernelSize / 2; i <= kernelSize / 2; i++) { for (int j = -kernelSize / 2; j <= kernelSize / 2; j++) { Color pixel = inputBitmap.GetPixel(x + i, y + j); int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); if (gray >= 128 && kernel[i + kernelSize / 2, j + kernelSize / 2] == 1) { flag = false; break; } } if (!flag) { break; } } if (flag) { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); } else { outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); } } } return outputBitmap; } /// /// Method that that use dilatation of the morphology of a bitmap /// /// The bitmap you want to use dilatation on /// The amount of dilatation you want (be carefull its expensive on ressources) /// The Bitmap after Dilatation public static Bitmap Dilatation(Bitmap inputBitmap, int kernelSize) { Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); int[,] kernel = new int[kernelSize, kernelSize]; for (int i = 0; i < kernelSize; i++) { for (int j = 0; j < kernelSize; j++) { kernel[i, j] = 1; } } for (int y = kernelSize / 2; y < inputBitmap.Height - kernelSize / 2; y++) { for (int x = kernelSize / 2; x < inputBitmap.Width - kernelSize / 2; x++) { bool flag = false; for (int i = -kernelSize / 2; i <= kernelSize / 2; i++) { for (int j = -kernelSize / 2; j <= kernelSize / 2; j++) { Color pixel = inputBitmap.GetPixel(x + i, y + j); int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); if (gray < 128 && kernel[i + kernelSize / 2, j + kernelSize / 2] == 1) { flag = true; break; } } if (flag) { break; } } if (flag) { outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); } else { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); } } } return outputBitmap; } } }","title":"OcrImage.cs"},{"location":"Code/Settings.html","text":"Settings.cs \ufeffusing System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.IO; namespace Test_Merge { public partial class Settings : Form { private string _grandPrixUrl = \"\"; private string _grandPrixName = \"\"; private int _grandPrixYear = 2000; private List _driverList = new List(); private F1TVEmulator Emulator = null; private ConfigurationTool Config = null; private bool CreatingZone = false; private Point ZoneP1; private Point ZoneP2; private bool CreatingWindow = false; private Point WindowP1; private Point WindowP2; List WindowsToAdd = new List(); public string GrandPrixUrl { get => _grandPrixUrl; private set => _grandPrixUrl = value; } public string GrandPrixName { get => _grandPrixName; private set => _grandPrixName = value; } public int GrandPrixYear { get => _grandPrixYear; private set => _grandPrixYear = value; } public List DriverList { get => _driverList; private set => _driverList = value; } public Settings() { InitializeComponent(); Load(); } private void Load() { RefreshUI(); } private void RefreshUI() { lsbDrivers.DataSource = null; lsbDrivers.DataSource = DriverList; if (Directory.Exists(ConfigurationTool.CONFIGS_FOLDER_NAME)) { lsbPresets.DataSource = null; lsbPresets.DataSource = Directory.GetFiles(ConfigurationTool.CONFIGS_FOLDER_NAME); } if (CreatingZone) { if (ZoneP1 == new Point(-1, -1)) { lblZonePointsRemaning.Text = \"2 points Remaining\"; } else { lblZonePointsRemaning.Text = \"1 point Remaining\"; } } else { lblZonePointsRemaning.Text = \"\"; } if (CreatingWindow) { if (WindowP1 == new Point(-1, -1)) { lblWindowPointsRemaining.Text = \"2 points Remaining\"; } else { lblWindowPointsRemaining.Text = \"1 point Remaining\"; } lblWindowPointsRemaining.Text = ConfigurationTool.NUMBER_OF_ZONES - WindowsToAdd.Count() + \" Windows remaining\"; } else { lblWindowPointsRemaining.Text = \"\"; lblWindowsRemaining.Text = \"\"; } if (Config != null) { pbxMain.Image = Config.MainZone.Draw(); if(Config.MainZone.Zones.Count > 0) pbxDriverZone.Image = Config.MainZone.Zones[0].Draw(); } } private void CreateNewZone(Point p1, Point p2) { Rectangle dimensions = CreateAbsoluteRectangle(p1, p2); Config = new ConfigurationTool((Bitmap)pbxMain.Image, dimensions); RefreshUI(); } private void CreateWindows(List dimensions) { if (Config != null) { Config.AddWindows(dimensions); } } private void tbxGpUrl_TextChanged(object sender, EventArgs e) { GrandPrixUrl = tbxGpUrl.Text; } private void tbxGpName_TextChanged(object sender, EventArgs e) { GrandPrixName = tbxGpName.Text; } private void tbxGpYear_TextChanged(object sender, EventArgs e) { int year; try { year = Convert.ToInt32(tbxGpYear.Text); } catch { year = 1545; } GrandPrixYear = year; } private void btnAddDriver_Click(object sender, EventArgs e) { string newDriver = tbxDriverName.Text; DriverList.Add(newDriver); tbxDriverName.Text = \"\"; RefreshUI(); } private void btnRemoveDriver_Click(object sender, EventArgs e) { if (lsbDrivers.SelectedIndex >= 0) { DriverList.RemoveAt(lsbDrivers.SelectedIndex); } RefreshUI(); } private void SwitchZoneCreation() { if (CreatingZone) { CreatingZone = false; lblZonePointsRemaning.Text = \"\"; } else { CreatingZone = true; if (Config != null) Config.ResetMainZone(); if (CreatingWindow) SwitchWindowCreation(); if (Emulator != null && Emulator.Ready) { Config = null; pbxMain.Image = Emulator.Screenshot(); } ZoneP1 = new Point(-1, -1); ZoneP2 = new Point(-1, -1); lblZonePointsRemaning.Text = \"2 Points left\"; } RefreshUI(); } private void SwitchWindowCreation() { if (CreatingWindow) { CreatingWindow = false; } else { CreatingWindow = true; if (Config != null) Config.ResetWindows(); if (CreatingZone) SwitchZoneCreation(); WindowP1 = new Point(-1, -1); WindowP2 = new Point(-1, -1); WindowsToAdd = new List(); } RefreshUI(); } private void btnCreatZone_Click(object sender, EventArgs e) { SwitchZoneCreation(); } private void btnCreateWindow_Click(object sender, EventArgs e) { SwitchWindowCreation(); } private void pbxMain_MouseClick(object sender, MouseEventArgs e) { if (CreatingZone && pbxMain.Image != null) { //Point coordinates = pbxMain.PointToClient(new Point(MousePosition.X, MousePosition.Y)); Point coordinates = e.Location; float xOffset = (float)pbxMain.Image.Width / (float)pbxMain.Width; float yOffset = (float)pbxMain.Image.Height / (float)pbxMain.Height; Point newPoint = new Point(Convert.ToInt32((float)coordinates.X * xOffset), Convert.ToInt32((float)coordinates.Y * yOffset)); //MessageBox.Show(\"Coordinates\" + Environment.NewLine + \"Old : \" + coordinates.ToString() + Environment.NewLine + \"New : \" + newPoint.ToString()); if (ZoneP1 == new Point(-1, -1)) { ZoneP1 = newPoint; } else { ZoneP2 = newPoint; CreateNewZone(ZoneP1, ZoneP2); SwitchZoneCreation(); } RefreshUI(); } } private void pbxMain_Click(object sender, EventArgs e) { //Not the right one to use visibly } private void pbxDriverZone_MouseClick(object sender, MouseEventArgs e) { if (CreatingWindow && pbxDriverZone.Image != null) { Point coordinates = e.Location; float xOffset = (float)pbxDriverZone.Image.Width / (float)pbxDriverZone.Width; float yOffset = (float)pbxDriverZone.Image.Height / (float)pbxDriverZone.Height; Point newPoint = new Point(Convert.ToInt32((float)coordinates.X * xOffset), Convert.ToInt32((float)coordinates.Y * yOffset)); if (WindowP1 == new Point(-1, -1)) { WindowP1 = newPoint; } else { WindowP2 = newPoint; WindowsToAdd.Add(CreateAbsoluteRectangle(WindowP1, WindowP2)); if (WindowsToAdd.Count < ConfigurationTool.NUMBER_OF_ZONES) { WindowP1 = new Point(-1, -1); WindowP2 = new Point(-1, -1); } else { WindowP1 = new Point(WindowP1.X, 0); WindowP2 = new Point(WindowP2.X, pbxDriverZone.Image.Height); CreateWindows(WindowsToAdd); SwitchWindowCreation(); } } RefreshUI(); } } private void pbxDriverZone_Click(object sender, EventArgs e) { //Not the right one to use visibly } private Rectangle CreateAbsoluteRectangle(Point p1, Point p2) { Point newP1 = new Point(); Point newP2 = new Point(); if (p1.X < p2.X) { newP1.X = p1.X; newP2.X = p2.X; } else { newP1.X = p2.X; newP2.X = p1.X; } if (p1.Y < p2.Y) { newP1.Y = p1.Y; newP2.Y = p2.Y; } else { newP1.Y = p2.Y; newP2.Y = p1.Y; } return new Rectangle(newP1.X, newP1.Y, newP2.X - newP1.X, newP2.Y - newP1.Y); } private async void btnRefresh_Click(object sender, EventArgs e) { btnRefresh.Enabled = false; if (Emulator == null || Emulator.GrandPrixUrl != tbxGpUrl.Text) { Emulator = new F1TVEmulator(tbxGpUrl.Text); } if (!Emulator.Ready) { Task start = Task.Run(() => Emulator.Start()); int errorCode = await start; if (errorCode != 0) { string message; switch (errorCode) { case 101: message = \"Error \" + errorCode + \" Could not start the driver. It could be because an other instance is runnin make sure you closed them all before trying again\"; break; case 102: message = \"Error \" + errorCode + \" Could not navigate on the F1TV site. Make sure the correct URL has been given and that you logged from chrome. It can take a few minutes to update\"; break; case 103: message = \"Error \" + errorCode + \" The url is not a valid url\"; break; case 104: message = \"Error \" + errorCode + \" The url is not a valid url\"; break; case 105: message = \"Error \" + errorCode + \" There has been an error trying to emulate button presses. Please try again\"; break; case 106: message = \"Error \" + errorCode + \" There has been an error trying to emulate button presses. Please try again\"; break; default: message = \"Could not start the emulator Error \" + errorCode; break; } MessageBox.Show(message); } else { pbxMain.Image = Emulator.Screenshot(); } } else { pbxMain.Image = Emulator.Screenshot(); } btnRefresh.Enabled = true; } private void Settings_FormClosing(object sender, FormClosingEventArgs e) { if (Emulator != null) { Emulator.Stop(); } } private void btnResetDriver_Click(object sender, EventArgs e) { if (Emulator != null) { Emulator.ResetDriver(); } } private void btnSavePreset_Click(object sender, EventArgs e) { string presetName = tbxPresetName.Text; if (Config != null) { Config.SaveToJson(DriverList,presetName); } RefreshUI(); } private void lsbPresets_SelectedIndexChanged(object sender, EventArgs e) { //Nothing } private void btnLoadPreset_Click(object sender, EventArgs e) { if (lsbPresets.SelectedIndex >= 0 && pbxMain.Image != null) { try { Reader reader = new Reader(lsbPresets.Items[lsbPresets.SelectedIndex].ToString(), (Bitmap)pbxMain.Image,false); //MainZones #0 is the big main zone containing driver zones Config = new ConfigurationTool((Bitmap)pbxMain.Image, reader.MainZones[0].Bounds); Config.MainZone = reader.MainZones[0]; DriverList = reader.Drivers; } catch (Exception ex) { MessageBox.Show(\"Could not load the settings error :\" + ex); } RefreshUI(); } } } }","title":"Settings.cs"},{"location":"Code/Settings.html#settingscs","text":"\ufeffusing System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.IO; namespace Test_Merge { public partial class Settings : Form { private string _grandPrixUrl = \"\"; private string _grandPrixName = \"\"; private int _grandPrixYear = 2000; private List _driverList = new List(); private F1TVEmulator Emulator = null; private ConfigurationTool Config = null; private bool CreatingZone = false; private Point ZoneP1; private Point ZoneP2; private bool CreatingWindow = false; private Point WindowP1; private Point WindowP2; List WindowsToAdd = new List(); public string GrandPrixUrl { get => _grandPrixUrl; private set => _grandPrixUrl = value; } public string GrandPrixName { get => _grandPrixName; private set => _grandPrixName = value; } public int GrandPrixYear { get => _grandPrixYear; private set => _grandPrixYear = value; } public List DriverList { get => _driverList; private set => _driverList = value; } public Settings() { InitializeComponent(); Load(); } private void Load() { RefreshUI(); } private void RefreshUI() { lsbDrivers.DataSource = null; lsbDrivers.DataSource = DriverList; if (Directory.Exists(ConfigurationTool.CONFIGS_FOLDER_NAME)) { lsbPresets.DataSource = null; lsbPresets.DataSource = Directory.GetFiles(ConfigurationTool.CONFIGS_FOLDER_NAME); } if (CreatingZone) { if (ZoneP1 == new Point(-1, -1)) { lblZonePointsRemaning.Text = \"2 points Remaining\"; } else { lblZonePointsRemaning.Text = \"1 point Remaining\"; } } else { lblZonePointsRemaning.Text = \"\"; } if (CreatingWindow) { if (WindowP1 == new Point(-1, -1)) { lblWindowPointsRemaining.Text = \"2 points Remaining\"; } else { lblWindowPointsRemaining.Text = \"1 point Remaining\"; } lblWindowPointsRemaining.Text = ConfigurationTool.NUMBER_OF_ZONES - WindowsToAdd.Count() + \" Windows remaining\"; } else { lblWindowPointsRemaining.Text = \"\"; lblWindowsRemaining.Text = \"\"; } if (Config != null) { pbxMain.Image = Config.MainZone.Draw(); if(Config.MainZone.Zones.Count > 0) pbxDriverZone.Image = Config.MainZone.Zones[0].Draw(); } } private void CreateNewZone(Point p1, Point p2) { Rectangle dimensions = CreateAbsoluteRectangle(p1, p2); Config = new ConfigurationTool((Bitmap)pbxMain.Image, dimensions); RefreshUI(); } private void CreateWindows(List dimensions) { if (Config != null) { Config.AddWindows(dimensions); } } private void tbxGpUrl_TextChanged(object sender, EventArgs e) { GrandPrixUrl = tbxGpUrl.Text; } private void tbxGpName_TextChanged(object sender, EventArgs e) { GrandPrixName = tbxGpName.Text; } private void tbxGpYear_TextChanged(object sender, EventArgs e) { int year; try { year = Convert.ToInt32(tbxGpYear.Text); } catch { year = 1545; } GrandPrixYear = year; } private void btnAddDriver_Click(object sender, EventArgs e) { string newDriver = tbxDriverName.Text; DriverList.Add(newDriver); tbxDriverName.Text = \"\"; RefreshUI(); } private void btnRemoveDriver_Click(object sender, EventArgs e) { if (lsbDrivers.SelectedIndex >= 0) { DriverList.RemoveAt(lsbDrivers.SelectedIndex); } RefreshUI(); } private void SwitchZoneCreation() { if (CreatingZone) { CreatingZone = false; lblZonePointsRemaning.Text = \"\"; } else { CreatingZone = true; if (Config != null) Config.ResetMainZone(); if (CreatingWindow) SwitchWindowCreation(); if (Emulator != null && Emulator.Ready) { Config = null; pbxMain.Image = Emulator.Screenshot(); } ZoneP1 = new Point(-1, -1); ZoneP2 = new Point(-1, -1); lblZonePointsRemaning.Text = \"2 Points left\"; } RefreshUI(); } private void SwitchWindowCreation() { if (CreatingWindow) { CreatingWindow = false; } else { CreatingWindow = true; if (Config != null) Config.ResetWindows(); if (CreatingZone) SwitchZoneCreation(); WindowP1 = new Point(-1, -1); WindowP2 = new Point(-1, -1); WindowsToAdd = new List(); } RefreshUI(); } private void btnCreatZone_Click(object sender, EventArgs e) { SwitchZoneCreation(); } private void btnCreateWindow_Click(object sender, EventArgs e) { SwitchWindowCreation(); } private void pbxMain_MouseClick(object sender, MouseEventArgs e) { if (CreatingZone && pbxMain.Image != null) { //Point coordinates = pbxMain.PointToClient(new Point(MousePosition.X, MousePosition.Y)); Point coordinates = e.Location; float xOffset = (float)pbxMain.Image.Width / (float)pbxMain.Width; float yOffset = (float)pbxMain.Image.Height / (float)pbxMain.Height; Point newPoint = new Point(Convert.ToInt32((float)coordinates.X * xOffset), Convert.ToInt32((float)coordinates.Y * yOffset)); //MessageBox.Show(\"Coordinates\" + Environment.NewLine + \"Old : \" + coordinates.ToString() + Environment.NewLine + \"New : \" + newPoint.ToString()); if (ZoneP1 == new Point(-1, -1)) { ZoneP1 = newPoint; } else { ZoneP2 = newPoint; CreateNewZone(ZoneP1, ZoneP2); SwitchZoneCreation(); } RefreshUI(); } } private void pbxMain_Click(object sender, EventArgs e) { //Not the right one to use visibly } private void pbxDriverZone_MouseClick(object sender, MouseEventArgs e) { if (CreatingWindow && pbxDriverZone.Image != null) { Point coordinates = e.Location; float xOffset = (float)pbxDriverZone.Image.Width / (float)pbxDriverZone.Width; float yOffset = (float)pbxDriverZone.Image.Height / (float)pbxDriverZone.Height; Point newPoint = new Point(Convert.ToInt32((float)coordinates.X * xOffset), Convert.ToInt32((float)coordinates.Y * yOffset)); if (WindowP1 == new Point(-1, -1)) { WindowP1 = newPoint; } else { WindowP2 = newPoint; WindowsToAdd.Add(CreateAbsoluteRectangle(WindowP1, WindowP2)); if (WindowsToAdd.Count < ConfigurationTool.NUMBER_OF_ZONES) { WindowP1 = new Point(-1, -1); WindowP2 = new Point(-1, -1); } else { WindowP1 = new Point(WindowP1.X, 0); WindowP2 = new Point(WindowP2.X, pbxDriverZone.Image.Height); CreateWindows(WindowsToAdd); SwitchWindowCreation(); } } RefreshUI(); } } private void pbxDriverZone_Click(object sender, EventArgs e) { //Not the right one to use visibly } private Rectangle CreateAbsoluteRectangle(Point p1, Point p2) { Point newP1 = new Point(); Point newP2 = new Point(); if (p1.X < p2.X) { newP1.X = p1.X; newP2.X = p2.X; } else { newP1.X = p2.X; newP2.X = p1.X; } if (p1.Y < p2.Y) { newP1.Y = p1.Y; newP2.Y = p2.Y; } else { newP1.Y = p2.Y; newP2.Y = p1.Y; } return new Rectangle(newP1.X, newP1.Y, newP2.X - newP1.X, newP2.Y - newP1.Y); } private async void btnRefresh_Click(object sender, EventArgs e) { btnRefresh.Enabled = false; if (Emulator == null || Emulator.GrandPrixUrl != tbxGpUrl.Text) { Emulator = new F1TVEmulator(tbxGpUrl.Text); } if (!Emulator.Ready) { Task start = Task.Run(() => Emulator.Start()); int errorCode = await start; if (errorCode != 0) { string message; switch (errorCode) { case 101: message = \"Error \" + errorCode + \" Could not start the driver. It could be because an other instance is runnin make sure you closed them all before trying again\"; break; case 102: message = \"Error \" + errorCode + \" Could not navigate on the F1TV site. Make sure the correct URL has been given and that you logged from chrome. It can take a few minutes to update\"; break; case 103: message = \"Error \" + errorCode + \" The url is not a valid url\"; break; case 104: message = \"Error \" + errorCode + \" The url is not a valid url\"; break; case 105: message = \"Error \" + errorCode + \" There has been an error trying to emulate button presses. Please try again\"; break; case 106: message = \"Error \" + errorCode + \" There has been an error trying to emulate button presses. Please try again\"; break; default: message = \"Could not start the emulator Error \" + errorCode; break; } MessageBox.Show(message); } else { pbxMain.Image = Emulator.Screenshot(); } } else { pbxMain.Image = Emulator.Screenshot(); } btnRefresh.Enabled = true; } private void Settings_FormClosing(object sender, FormClosingEventArgs e) { if (Emulator != null) { Emulator.Stop(); } } private void btnResetDriver_Click(object sender, EventArgs e) { if (Emulator != null) { Emulator.ResetDriver(); } } private void btnSavePreset_Click(object sender, EventArgs e) { string presetName = tbxPresetName.Text; if (Config != null) { Config.SaveToJson(DriverList,presetName); } RefreshUI(); } private void lsbPresets_SelectedIndexChanged(object sender, EventArgs e) { //Nothing } private void btnLoadPreset_Click(object sender, EventArgs e) { if (lsbPresets.SelectedIndex >= 0 && pbxMain.Image != null) { try { Reader reader = new Reader(lsbPresets.Items[lsbPresets.SelectedIndex].ToString(), (Bitmap)pbxMain.Image,false); //MainZones #0 is the big main zone containing driver zones Config = new ConfigurationTool((Bitmap)pbxMain.Image, reader.MainZones[0].Bounds); Config.MainZone = reader.MainZones[0]; DriverList = reader.Drivers; } catch (Exception ex) { MessageBox.Show(\"Could not load the settings error :\" + ex); } RefreshUI(); } } } }","title":"Settings.cs"},{"location":"Code/recoverCookiesCSV.html","text":"recoverCookiesCSV.py # Rohmer Maxime # RecoverCookies.py # Little script that recovers the cookies stored in the chrome sqlite database and then decrypts them using the key stored in the chrome files # This script has been created to be used by an other programm or for the data to not be used directly. This is why it stores all the decoded cookies in a csv. (Btw could be smart for the end programm to delete the csv after using it) # Parts of this cript have been created with the help of ChatGPT import os import json import base64 import sqlite3 import win32crypt from Cryptodome.Cipher import AES from pathlib import Path import csv def get_master_key(): with open( os.getenv(\"localappdata\") + \"\\\\Google\\\\Chrome\\\\User Data\\\\Local State\", \"r\" ) as f: local_state = f.read() local_state = json.loads(local_state) master_key = base64.b64decode(local_state[\"os_crypt\"][\"encrypted_key\"]) master_key = master_key[5:] # removing DPAPI master_key = win32crypt.CryptUnprotectData(master_key, None, None, None, 0)[1] print(\"MASTER KEY :\") print(master_key) print(len(master_key)) return master_key def decrypt_payload(cipher, payload): return cipher.decrypt(payload) def generate_cipher(aes_key, iv): return AES.new(aes_key, AES.MODE_GCM, iv) def decrypt_password(buff, master_key): try: iv = buff[3:15] payload = buff[15:] cipher = generate_cipher(master_key, iv) decrypted_pass = decrypt_payload(cipher, payload) decrypted_pass = decrypted_pass[:-16].decode() # remove suffix bytes return decrypted_pass except Exception: # print(\"Probably saved password from Chrome version older than v80\\n\") # print(str(e)) return \"Chrome < 80\" master_key = get_master_key() cookies_path = Path( os.getenv(\"localappdata\") + \"\\\\Google\\\\Chrome\\\\User Data\\\\Default\\\\Network\\\\Cookies\" ) if not cookies_path.exists(): raise ValueError(\"Cookies file not found\") with sqlite3.connect(cookies_path) as connection: connection.row_factory = sqlite3.Row cursor = connection.cursor() cursor.execute(\"SELECT * FROM cookies\") with open('cookies.csv', 'a', newline='') as csvfile: fieldnames = ['host_key', 'name', 'value', 'path', 'expires_utc', 'is_secure', 'is_httponly'] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) if csvfile.tell() == 0: writer.writeheader() for row in cursor.fetchall(): decrypted_value = decrypt_password(row[\"encrypted_value\"], master_key) writer.writerow({ 'host_key': row[\"host_key\"], 'name': row[\"name\"], 'value': decrypted_value, 'path': row[\"path\"], 'expires_utc': row[\"expires_utc\"], 'is_secure': row[\"is_secure\"], 'is_httponly': row[\"is_httponly\"] }) print(\"Finished CSV\")","title":"recoverCookiesCSV.py"},{"location":"Code/recoverCookiesCSV.html#recovercookiescsvpy","text":"# Rohmer Maxime # RecoverCookies.py # Little script that recovers the cookies stored in the chrome sqlite database and then decrypts them using the key stored in the chrome files # This script has been created to be used by an other programm or for the data to not be used directly. This is why it stores all the decoded cookies in a csv. (Btw could be smart for the end programm to delete the csv after using it) # Parts of this cript have been created with the help of ChatGPT import os import json import base64 import sqlite3 import win32crypt from Cryptodome.Cipher import AES from pathlib import Path import csv def get_master_key(): with open( os.getenv(\"localappdata\") + \"\\\\Google\\\\Chrome\\\\User Data\\\\Local State\", \"r\" ) as f: local_state = f.read() local_state = json.loads(local_state) master_key = base64.b64decode(local_state[\"os_crypt\"][\"encrypted_key\"]) master_key = master_key[5:] # removing DPAPI master_key = win32crypt.CryptUnprotectData(master_key, None, None, None, 0)[1] print(\"MASTER KEY :\") print(master_key) print(len(master_key)) return master_key def decrypt_payload(cipher, payload): return cipher.decrypt(payload) def generate_cipher(aes_key, iv): return AES.new(aes_key, AES.MODE_GCM, iv) def decrypt_password(buff, master_key): try: iv = buff[3:15] payload = buff[15:] cipher = generate_cipher(master_key, iv) decrypted_pass = decrypt_payload(cipher, payload) decrypted_pass = decrypted_pass[:-16].decode() # remove suffix bytes return decrypted_pass except Exception: # print(\"Probably saved password from Chrome version older than v80\\n\") # print(str(e)) return \"Chrome < 80\" master_key = get_master_key() cookies_path = Path( os.getenv(\"localappdata\") + \"\\\\Google\\\\Chrome\\\\User Data\\\\Default\\\\Network\\\\Cookies\" ) if not cookies_path.exists(): raise ValueError(\"Cookies file not found\") with sqlite3.connect(cookies_path) as connection: connection.row_factory = sqlite3.Row cursor = connection.cursor() cursor.execute(\"SELECT * FROM cookies\") with open('cookies.csv', 'a', newline='') as csvfile: fieldnames = ['host_key', 'name', 'value', 'path', 'expires_utc', 'is_secure', 'is_httponly'] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) if csvfile.tell() == 0: writer.writeheader() for row in cursor.fetchall(): decrypted_value = decrypt_password(row[\"encrypted_value\"], master_key) writer.writerow({ 'host_key': row[\"host_key\"], 'name': row[\"name\"], 'value': decrypted_value, 'path': row[\"path\"], 'expires_utc': row[\"expires_utc\"], 'is_secure': row[\"is_secure\"], 'is_httponly': row[\"is_httponly\"] }) print(\"Finished CSV\")","title":"recoverCookiesCSV.py"}]} \ No newline at end of file +{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"index.html","text":"Rapport Track Trends V1.0 Rohmer Maxime Travail de dipl\u00f4me Technicien ES 2023 Introduction R\u00e9sum\u00e9 Track Trends est un outil de r\u00e9cup\u00e9ration et d'analyse de donn\u00e9es de courses de Formule 1. Pour le contexte, en dehors des cours, j'exerce diff\u00e9rentes activit\u00e9s dont celle de Live Ticker F1 pour le 20 minutes. Pour m'aider dans ce travail, j'utilise actuellement la F1TV \u00e0 laquelle je suis abonn\u00e9 qui me propose non seulement un feed vid\u00e9o de meilleure qualit\u00e9 avec des commentaires plus pertinents que ceux de la RTS mais qui aussi me permet d'acc\u00e9der \u00e0 un feed vid\u00e9o tr\u00e8s important : la chaine data. Ce dernier ressemble \u00e0 cela : \"Screenshot du feed data de la f1tv\" (Attention ce n'est pas un joli tableau HTML, mais bien une vid\u00e9o qui contient un tableau.) Sauf que toutes les informations sont \u00e9tal\u00e9es p\u00eale-m\u00eale sans hi\u00e9rarchie ce qui fait que cela me prendrait trop de temps de tout d\u00e9chiffrer \u00e0 chaque fois, ce qui me fait rater des choses int\u00e9ressantes. Le but du projet est donc de fournir un outil qui hi\u00e9rarchise et affiche diff\u00e9remment les donn\u00e9es pour faciliter leur lecture et me permettre de faire de meilleurs commentaires. Abstract Track Trends is a Formula 1 data is a tool that displays and interpret data. To understand everything,first ,a little bit of context. In my free time I have multiple activities and one is to be the Live Ticker F1 for the local journal \"20 minutes\" (Owned by Tamedia). to help me in this work I'm currently using the F1TV to which I'm currently subscribed because it provides me with a better video feed with better commentary than the ones from the RTS (in my opinion) but also because it gives me access to a very important video feed : the data channel You can see in the chapter above an example of the F1TV DATA CHANNEL. [Note : Even tough it looks like a pretty HTML table on wich you could easely get infos... Its not. Its a video feed] You can see a lot of data all well and good BUT! All the data is displayed the same in a big table which make it really hard to read totally in a hurry, which means that I miss a lot of useful information. The point of the project then is to provide a tool that can display those data by taking into account their relevance. So for example a driver that is 10s away from everyone and that is doing some normal lapTimes will be less displayed or even not displayed at all so I can focus on the drivers that are battling each others. This tool would help me not miss the battles and details that are happening in the back and therefore not being broadcasted on TV.And it could be a usefull tool for anyone who wants a better insight of how the race is going by looking at the data. This kind of project already exists in the form of the AWS tool \"F1 Insight\" but it is not avaible to the public. We can only see some of its predictions (that are trash) and data dumps in the live feed when the TV directors feel like it. Description du besoin Comme expliqu\u00e9 dans le r\u00e9sum\u00e9, je suis Live Ticker F1. Mais pour mieux comprendre le besoin que j'ai, je pense qu'il est pertinent de comprendre comment je travaille. Pendant un Grand Prix de Formule 1 j'ai plusieurs t\u00e2ches \u00e0 effectuer : Suivre les diff\u00e9rents \u00e9v\u00e8nements du Grand Prix Changer le titre et la photo de titre du Live Chercher des Tweets ou des Images \u00e0 int\u00e9grer Ecrire les commentaires en faisant attention \u00e0 dire ce qu'il se passe mais aussi l\u2019expliquer, ce que cela implique, mais aussi ce que cela veut dire pour le reste de la course. Comprendre et expliquer les strat\u00e9gies Tout cela toutes les cinq minutes max... Cela veut dire que je dois \u00eatre le plus rapide possible quand je cherche des informations. Et comme le tableau en comporte trop et bien, je suis oblig\u00e9 de le lire en diagonale. Par exemple dans le tableau, les infos que je cherche le plus sont : Le nombre de places gagn\u00e9es (surtout au d\u00e9part) Les \u00e9carts entre les pilotes (surtout ceux qui sont en dessous de deux secondes). Les pneus de chaque pilote et combien de tours, ils ont fait dessus Les temps d'arr\u00eats aux stands Les temps au tour (surtout pour la strat\u00e9gie) Mais pleins d'autres informations existent si on les recoupe sur plusieurs tours. Un outil qui permettrait de mettre en \u00e9vidence les informations importantes serait donc une tr\u00e8s grosse plus-value pour mon travail et s'il est facile \u00e0 installer et \u00e0 utiliser, il se pourrait qu'il devienne indispensable. Cahier des charges Il s'agit d'une version coup\u00e9e du cahier des charges qui ne contient pas l'explication du contexte. Mais l'original est disponible sur ce site \u00e9galement. Il est toutefois normal d'y voir des choses r\u00e9p\u00e9t\u00e9es ou l\u00e9g\u00e8rement diff\u00e9rentes, en effet, il n'a pas \u00e9t\u00e9 \u00e9crit en m\u00eame temps que le reste de ce document. Projet Un outil de style compagnon sous forme d'application C# Windows Form qui r\u00e9cup\u00e8re en temps r\u00e9el les informations de la course et affiche les informations les plus importantes. Le but est non seulement de faciliter mon job, mais aussi faire en sorte d'am\u00e9liorer la plus-value de mon travail en me permettant de fournir des commentaires qui ne sont pas disponibles pour le tout venant qui regarde simplement le flux RTS. Exemples : Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand Maintenant afficher diff\u00e9remment les infos, c'est sympa, mais cela serait encore mieux de traiter ces data et de permettre des petites pr\u00e9dictions. Exemples : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents R\u00e9alisation Malheureusement, la Formula 1 Management ne propose aucune API publique qui puisse nous permettre de faire ce projet \"simplement\". La raison la plus probable \u00e9tant qu'Amazon avec son service AWS propose exactement ce genre de services pour le flux t\u00e9l\u00e9vis\u00e9 et il doit y avoir un contrat d'exclusivit\u00e9. Il existe des API \"Pirates\" faites par la communaut\u00e9, le probl\u00e8me est qu'elles ne sont pas forc\u00e9ment des plus pratiques \u00e0 utiliser. Mais comme je poss\u00e8de un abonnement premium ++ \u00e0 la F1TV, j'ai acc\u00e8s pour chaque grand prix \u00e0 un flux vid\u00e9o nomm\u00e9 : DATA F1 CHANNEL Qui ressemble \u00e0 \u00e7a : \"Exemple de la Data Channel\" Donc la seule fa\u00e7on que je vois de r\u00e9cup\u00e9rer ces donn\u00e9es est de les prendre directement sur ce feed. M\u00eame si le but final de l'application est de faire pleins de choses super avec les datas, le gros du projet va surtout \u00eatre la r\u00e9cup\u00e9ration des donn\u00e9es et leur stockage. Les donn\u00e9es viennent du flux vid\u00e9o et ainsi dans un premier temps, il va falloir r\u00e9cup\u00e9rer d'une mani\u00e8re ou d'une autre des images qui viennent d'un grand prix en direct ou en rediffusion. Ensuite, dans un second temps, il faut lire les informations directement sur l'image en utilisant une librairie pr\u00e9vue pour (exemple Tesseract) et v\u00e9rifier l'int\u00e9grit\u00e9 de ces derni\u00e8res pour qu'on puisse ensuite les stocker. Dans un troisi\u00e8me temps, il faut stocker toutes ces donn\u00e9es dans une forme qui permette d'aller facilement faire des requ\u00eates de r\u00e9cup\u00e9ration et d\u00e9j\u00e0 pr\u00e9parer des m\u00e9thodes qui permettent de r\u00e9cup\u00e9rer des infos importantes (ex : la moyenne des cinq derniers tours, le temps moyen d'arr\u00eat etc.) pour faciliter la derni\u00e8re \u00e9tape Quand tout cela est fait, on peut ensuite s'amuser un peu avec les Data. La derni\u00e8re \u00e9tape est donc l'affichage. L'id\u00e9e est de cr\u00e9er une Windows Form qui contienne toutes ces informations dans un format beaucoup plus lisible et avec laquelle on pourrait interagir pour permettre de plus facilement commenter les Grands Prix. (exemple plus bas avec un croquis de ce \u00e0 quoi l'application pourrait ressembler) Voici la liste des donn\u00e9es qui pourraient \u00eatre affich\u00e9es (Non contractuel, simplement des id\u00e9es). Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand La moyenne de temps que les pilotes perdent dans les stands La performance moyenne des 5 types de pneus La moyenne de temps de chaque pilote sur le pneu actuel Le nombre de points que chaque pilote gagnerait selon sa position Le classement de la course Voire m\u00eame si c'est possible : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents Pr\u00e9dire les temps au tour de chaque pilote selon l'usure des pneus Voici un exemple d'interface possible pour une page : \"Protype de l'app fait sur Figma\" Cas d'utilisation '*'On va consid\u00e9rer que tous les user ont un abonnement F1 TV PRO Un user veut r\u00e9cup\u00e9rer les data : Il ouvre son navigateur et lance la page DATA de la F1 TV Il calibre la capture des data via le programme (pour la premi\u00e8re utilisation). Il confirme que les donn\u00e9es initiales sont bonnes (pour la premi\u00e8re utilisation). Il regarde tranquille son Grand Prix Le programme r\u00e9cup\u00e8re les data : Il r\u00e9cup\u00e8re des images depuis la F1TV Il utilise Tesseract (ou autre) pour en r\u00e9cup\u00e9rer les infos. Il met ces infos dans un Objet Pilote, dans un Objet course avec un attribut tour pour hi\u00e9rarchiser les data Pour ce qui est de l'affichage, l'id\u00e9e est de faire une application C# comme on l'a appris \u00e0 l'\u00e9cole, mais avec assez de style pour qu'elle puisse \u00eatre agr\u00e9able \u00e0 utiliser. Quand le programme affiche les data : Il prend les donn\u00e9es venant directement de la F1TV. Il affiche diff\u00e9remment les donn\u00e9es pour permettre une meilleure lisibilit\u00e9 Il interpr\u00e8te avec des r\u00e8gles plut\u00f4t simples certaines data pour faire des minipr\u00e9dictions ou aider \u00e0 la lecture Il r\u00e9cup\u00e8re des infos d'autres courses pour les comparer et proposer des pr\u00e9dictions plus int\u00e9ressantes Difficult\u00e9s techniques R\u00e9cup\u00e9rer un flux vid\u00e9o plut\u00f4t propre malgr\u00e9 les contres mesures de la F1 TV pour en emp\u00eacher la lecture par un logiciel Si on doit passer par une capture d'\u00e9cran, trouver un moyen de stocker les donn\u00e9es de mani\u00e8re \u00e0 pr\u00e9voir que parfois un tour pourrait avoir plus de donn\u00e9es qu'un autre, que le user peut mettre pause, ou m\u00eame qu\u2019il revienne en arri\u00e8re. D\u00e9velopper des algorithmes pour r\u00e9cup\u00e9rer les donn\u00e9es comme les diff\u00e9rents pneus utilis\u00e9s ou l'activation du DRS ainsi que d\u00e9velopper des moyens de nettoyer les r\u00e9sultats de l'OCR (Par exemple utiliser diff\u00e9rents champs redondants pour comparer les r\u00e9sultats) Stocker les donn\u00e9es sur une base pour les traiter plus tard tout en pr\u00e9voyant un moyen de voir les stats live D\u00e9velopper des algorithmes de pr\u00e9diction qui prennent en compte d'anciennes courses pour tenter de pr\u00e9dire des choses comme les arr\u00eats aux stands par exemple. Diff\u00e9rences sur le cahier des charges [\u00c0 remplir dans les derni\u00e8res semaines du travail de dipl\u00f4me] Planning pr\u00e9visionnel Mes suiveurs m'ont demand\u00e9 un planning de type GANTT pour ce travail de dipl\u00f4me Je n'ai pas utilis\u00e9 un logiciel particulier pour faire ce dernier, mais je me suis inspir\u00e9 des principes fondamentaux d'un diagramme de ce type. Comme l'original a \u00e9t\u00e9 fait sur Excel, je ne peux pas vraiment l'ins\u00e9rer de mani\u00e8re lisible ici, mais il est disponible dans le dossier Planning. Mais voici un r\u00e9sum\u00e9 de son contenu : T\u00e2ches J'ai d\u00e9cid\u00e9 de d\u00e9composer mon planning en trois grands types de t\u00e2ches. Programmation Documentation Tests L'id\u00e9e est de permettre une meilleure lisibilit\u00e9 et de me permettre \u00e0 moi de me faire plus facilement \u00e0 l'id\u00e9e de ce qu'il m'attend. Voici la liste des t\u00e2ches par rubrique : PT Cette rubrique contient les t\u00e2ches qui n'ont pas leur place dans les trois cat\u00e9gories principales. PT1 / pr\u00e9paration au travail de dipl\u00f4me (2) Cette t\u00e2che est un peu hors cat\u00e9gorie, mais c'est normal, c'est une supert\u00e2che qui regroupe beaucoup de choses. C'est une t\u00e2che qui est planifi\u00e9e pour deux jours et qui normalement devrait \u00eatre faite les deux premiers jours du travail. Le but est de pr\u00e9parer tout ce qui peut \u00eatre pr\u00e9par\u00e9 en avance niveau documentation et mise en place pour ne pas avoir besoin de s'en soucier ensuite. DT Rubrique documentation qui contient toutes les t\u00e2ches en rapport de pr\u00e8s ou de loin avec la documentation du projet. DT1 Cr\u00e9ation du poster (1) Cette t\u00e2che consiste \u00e0 faire une version num\u00e9rique du poster qui soit en accord avec les consignes qu'on nous a donn\u00e9es. Le but est aussi et surtout de faire poster dont je sois fier et que je sois content de montrer. Il y a d\u00e9j\u00e0 des croquis de poster et j'ai clairement pr\u00e9vu de travailler sur \u00e7a pendant les vacances alors, je n'ai mis qu'un jour et je l'ai plac\u00e9 juste avant le rendu de ce dernier. DT2 Documentation Analyse de l'existant (2) Cette t\u00e2che est d\u00e9di\u00e9e \u00e0 l'\u00e9criture de la documentation et plus pr\u00e9cis\u00e9ment de l'analyse de l'existant. Comme il y a pas mal de technologies utilis\u00e9es dans mon projet, j'aimerais faire correctement un vrai debrief de pourquoi j'ai utilis\u00e9 l'une ou l'autre alors, j'ai assign\u00e9 deux jours dessus. DT3 Documentation Analyse organique (5) Cette t\u00e2che est la plus grosse dans la cat\u00e9gorie documentation. Il s'agit de documenter comment l'application fonctionne. J'y ai mis cinq jours et je pense que c'est un minimum car c'est dans cette t\u00e2che que je vais devoir d\u00e9tailler exactement comment fonctionne chaque partie du projet. Ces cinq jours sont \u00e9parpill\u00e9s sur le projet en g\u00e9n\u00e9ral \u00e0 la fin du d\u00e9veloppement de chaque grande partie de projet. Le but est de ne rien oublier et de ne pas avoir \u00e0 tout faire en m\u00eame temps. DT4 Documentation Analyse fonctionnelle (2) Cette t\u00e2che est d\u00e9j\u00e0 moins grosse, elle consiste \u00e0 documenter le fonctionnement de l'application et comment utiliser les composants que j'ai d\u00e9velopp\u00e9s. Je l'ai mis en fin de projet, car comme j'ai l'habitude de faire des analyses fonctionnelles plut\u00f4t pr\u00e9cises, le moindre changement dans l'UI peut tout rendre obsol\u00e8te. J'y ai mis deux jours, car j'aimerais correctement documenter avec de bonnes photos et sc\u00e9narios pour qu'on puisse voir toutes les possibilit\u00e9s de l'application. DT5 Documentation Tests (1) Cette t\u00e2che est un peu plus petite qu'elle ne le devrait. Elle concerne la documentation des diff\u00e9rents tests. Je n'y ai mis qu'un seul jour, car en r\u00e9alit\u00e9 les diff\u00e9rentes t\u00e2ches de tests contiennent aussi beaucoup de documentation, DT6 Documentation Reste (2) Cette t\u00e2che est une t\u00e2che un peu vague. Elle contient toutes les actions autres que j'aurai besoin de faire (Mise au propre, orthographe, g\u00e9n\u00e9ration de PDF ...). J'y ai mis deux jours pour avoir un peu de marge, car ce sont toujours des t\u00e2ches qui paraissent faciles, mais qui \u00e0 la fin prennent beaucoup de temps si on les fait bien. PT Rubrique programmation qui contient toutes les t\u00e2ches qui touchent \u00e0 la programmation et au d\u00e9veloppement de l'application. PT1 Programmation r\u00e9cup\u00e9ration des images (3) Cette t\u00e2che est estim\u00e9e \u00e0 seulement trois jours, il ne faut pas s'y m\u00e9prendre, c'est une des t\u00e2ches les plus dures et lourdes niveaux documentation en explications. Cependant, un POC (Proof Of Concept) assez avanc\u00e9 a d\u00e9j\u00e0 \u00e9t\u00e9 fait et donc cela permet de n'envisager que trois jours, car il suffit de l'impl\u00e9menter et de la paufinner. Cette t\u00e2che consiste \u00e0 prendre en entr\u00e9e un lien de Grand Prix et de sortir une image tous les x secondes de la page DATA. Cela peut sembler simple, mais pour le faire sans prendre d'espace d'\u00e9cran et ne demandant pas \u00e0 l'utilisateur de copier-coller quoi que ce soit o\u00f9 de donner ses identifiants F1TV c'est un challenge. Cela peut paraitre curieux alors de mettre cette t\u00e2che loin dans le planning m\u00eame si c'est la premi\u00e8re \u00e9tape du projet. Encore une fois cela s'explique avec le fait qu'il y a d\u00e9j\u00e0 un POC qui fonctionne \u00e0 peu pr\u00e8s et que donc pr\u00e9f\u00e8re commencer avec des t\u00e2ches plus incertaines dans le cas o\u00f9 elles prendraient plus de temps que pr\u00e9vu. PT2 Programmation OCR (5) Cette t\u00e2che consiste \u00e0 d\u00e9velopper la partie qui reconnait le texte sur les images. C'est tr\u00e8s certainement la t\u00e2che qui risque le plus de d\u00e9border car c'est celle qui est la plus complexe techniquement puisqu'elle demande non seulement la lecture sur image, mais aussi le d\u00e9veloppement d'algorithmes de traitement de cette donn\u00e9e pour \u00eatre s\u00fbr qu'elle a bien \u00e9t\u00e9 lue. J'y ai ainsi allou\u00e9 cinq jours, mais j'esp\u00e8re que j'arriverai \u00e0 gagner du temps sur les autres pour y allouer plus dans le planning effectif, car je suis convaincu que plus, on y passe du temps, meilleur sera le r\u00e9sultat. PT3 Programmation, stockage et mod\u00e8le (5) Cette partie est moins technique, mais concerne le stockage des donn\u00e9es que nous retourne la lecture des images. Mais elle va demander de la r\u00e9flexion et de l'intelligence de programmation, car il faut que cette partie anticipe les besoins de la vue et pr\u00e9pare un terrain fertile qui ne demande pas un refactor quand on passera au d\u00e9veloppement de la vue. C'est pour cela que je lui ai aussi assign\u00e9 cinq jours de travail et elle doit absolument \u00eatre commenc\u00e9e apr\u00e8s la lecture. PT4 Programmation Vue de l'APP (5) Cette partie peut \u00eatre horrible comme tr\u00e8s facile, cela d\u00e9pend compl\u00e8tement de la qualit\u00e9 du travail avant. Si le mod\u00e8le est parfait et que les donn\u00e9es sont int\u00e8gres, cela devrait \u00eatre plut\u00f4t simple de les afficher de mani\u00e8re int\u00e9ressante. Cependant, cette partie d\u00e9bordera s\u00fbrement un peu, car tout le temps gagn\u00e9 avec de bonnes donn\u00e9es sera utilis\u00e9 pour tenter de faire de la pr\u00e9diction. Pour ces raisons, je lui ai assign\u00e9 \u00e9galement cinq jours de travail et elle doit absolument \u00eatre faite apr\u00e8s le mod\u00e8le. PT5 Programmation mise en commun (3) Cette t\u00e2che est aussi un petit peu sp\u00e9ciale, car elle regroupe plusieurs choses. En gros, chaque partie de programmation sera s\u00fbrement assez ind\u00e9pendante et il faudra \u00e0 un moment faire un seul projet C# qui contient tout. Il est difficile d'estimer \u00e0 quel point cela va \u00eatre compliqu\u00e9 alors, j'ai \u00e9t\u00e9 conservateur et j'ai mis trois jours. TT Cette rubrique contient les t\u00e2ches qui sont uniquement des tests. La plupart des t\u00e2ches de programmations contiennent d\u00e9j\u00e0 des tests, mais certaines demandent une attention particuli\u00e8re. TT1 Tests OCR (2) Cette t\u00e2che est une des t\u00e2ches les plus importantes. Son but est de faire un protocole de tests complet qui permette de comparer les diff\u00e9rents algorithmes de reconnaissance de texte. Je l'ai mise apr\u00e8s la reconnaissance, mais m\u00eame maintenant en \u00e9crivant ces lignes, je me dis que dans le planning effectif, elle sera faite pendant la t\u00e2che de programmation. En effet, comment savoir si mon tout nouvel algorithme est r\u00e9ellement mieux que le pr\u00e9c\u00e9dent. Je pr\u00e9vois deux jours, car je pense que faire le dataset va prendre beaucoup de temps, il faut pr\u00e9voir un certain nombre d'images et de texte qui pourront ensuite \u00eatre donn\u00e9es sous forme de tests. C'est certes une t\u00e2che de test, mais c'est aussi de la programmation. TT2 Tests finaux (2) Cette t\u00e2che de tests est un peu vague, elle regroupe les diff\u00e9rents tests pour v\u00e9rifier que les donn\u00e9es sont bien affich\u00e9es correctement. Ce qui serait cool si j'ai du temps en fin de travail de dipl\u00f4me serait de faire un syst\u00e8me de test qui permet d'entrainer le programme \u00e0 mieux reconnaitre certaines choses comme des arr\u00eats aux stands si on lui donne les trois derni\u00e8res ann\u00e9es de grands Prix. J'ai mis une dur\u00e9e arbitraire de deux jours, mais je ne sais pas vraiment combien de temps cela va vraiment durer. Elle est \u00e9videmment \u00e0 effectuer une fois que tout est \u00e0 peu pr\u00e8s termin\u00e9. Planning effectif et diff\u00e9rences [A remplir dans les derni\u00e8res semaines du travail de dipl\u00f4me] Analyse fonctionnelle [A remplir au fur et \u00e0 mesure dans la seconde moiti\u00e9 du travail de dipl\u00f4me] Analyse Organique [A remplir au fur et \u00e0 mesure que les features sont d\u00e9velopp\u00e9es] R\u00e9cup\u00e9ration des images Voici la premi\u00e8re grande \u00e9tape du projet. Pour rappel, Amazon h\u00e9berge directement le site de la F1TV et poss\u00e8de les droits sur les donn\u00e9es de la F1. C'est sous le nom de AWS (le service d'h\u00e9bergement d'Amazon) que la firme apparait en tant que sponsor. On peut voir ce nom appara\u00eetre assez souvent quand on regarde un Grand Prix car comme ils ont la main-mise sur les donn\u00e9es ils peuvent ins\u00e8rer des bandeaux d'informations sur le flux public sur ce qu'il se passe voir m\u00eame faire des pr\u00e9dictions (Bien qu'un peu bancales) \"Exemple insertion AWS en GP\" Ce service s'appelle F1 Insights (Oui c'est un meilleur nom de projet que F1 Companion mais bon) et c'est, je pense, la raison pour laquelle on ne voit aucune API publique qui permette de correctement se renseigner en don\u00e9es en direct pendant un Grand Prix. Ils ont du d\u00e9gotter un juteux contrat pour s'occuper de toute l'infrastructure digitale de la F1 (du moins publique) en \u00e9change d'une exclusivit\u00e9 totale sur certaines choses comme les Data \"Exemple data d'AWS\" Evidemment je ne fais que conjecturer et ce que j'ai dit n'est pas \u00e0 prendre au pied de la lettre mais c'est une explication possible je pense de pourquoi il est si difficile de trouver des donn\u00e9es sur la F1 facilement en temps r\u00e9el. Il existe bien quelques API un peu bancales publiques, mais le probl\u00e8me c'est qu'elles ne sont vraiment pas suffisante et je ne peux pas leur faire confiance quand je commente. Ce qu'il m'aurait fallut c'est une API publique et officielle qui me permette d'\u00eatre sur que les donn\u00e9es sont les bonnes et qu'elles arrivent le plus vite possible. On pourrait croire que c'est impossible car cela n'existe pas comme je l'ai dit MAIS ! Ce n'est pas compl\u00eatement vrai. En effet depuis que je poss\u00e8de un abonnement \u00e0 la F1TV, il existe une source d'informations tr\u00e8s pr\u00e9cieuse qui m'aide \u00e9norm\u00e9ment dans mon quotidien de commentateur de Formule 1. La \"DATA CHANNEL\". La Data Channel est une page de la F1TV qui permet, pour chaque Grand Prix, de visualiser, sous la forme d'un flux vid\u00e9o, diff\u00e9rentes informations capitales sur la course. \"Exemple de Data Channel\" Le probl\u00e8me, c'est que comme je viens de le dire, ces donn\u00e9es ne sont pas accessibles comme un tableau HTML ou un flux RSS ou un tableau JSON. C'est un flux vid\u00e9o. Il faut savoir qu'entretenir une diffusion de flux vid\u00e9o en 1080P pendant deux heures accessible par des milliers d'abonn\u00e9s est EXTR\u00caMENT cher surtout quand on le compare \u00e0 simplement afficher les donn\u00e9es dans un tableau. Ce qui veut dire que ce choix est d\u00e9lib\u00e9r\u00e9 et a un sens au niveau \u00e9conomique. Je pense donc que c'est justement pour \u00e9viter que des petits malins puissent simplement venir scraper l'int\u00e9gralit\u00e9 des donn\u00e9es qu'ils proposent et fasse sa propre API. (C'est d'ailleurs un des sites avec la meilleure protection anti bot du monde) MAIS ce n'est pas par ce que les donn\u00e9es ne sont pas facile \u00e0 avoir qu'elles sont IMPOSSIBLE \u00e0 avoir. Et c'est la que ce projet entre en jeu. Mais pour d\u00e9coder les donn\u00e9es d'une image il faut dabord ... (roulement de tambours) ... Avoir des images ! Et c'est la que vient se glisser cette partie du projet. Comment faire ? Le but de ce segment est de se concentrer sur la r\u00e9cup\u00e8ration et la mise \u00e0 disposition pour le reste du programme, des images en direct de la F1TV dans la meilleure qualit\u00e9 possible et dans les meilleurs d\u00e9lais. Pour ce faire il y a plusieurs solutions : Reverse engeneer la F1TV pour acc\u00e8der directement au flux sans passer par la plateforme internet et pouvoir prendres images \u00e0 volont\u00e9. Avoir tout simplement une page de la F1TV ouverte sur un \u00e9cran et prendres des screenshots \u00e0 intervals r\u00e9guliers. Simuler un navigateur internet sans qu'il soit affich\u00e9 et le contr\u00f4ler automatiquement pour qu'il prenne des captures. La premi\u00e8re option aurait \u00e9t\u00e9 la plus \u00e9l\u00e9gante mais lors d'un POC que je tentais de r\u00e9aliser je me suis rendu compte que cela serait un peu trop compliqu\u00e9 et long \u00e0 faire. Sans compter le fait que les rediffusions de Grand Prix ne sont pas g\u00e8r\u00e9es de la m\u00eame mani\u00e8re que les diffusions en live. Et que pour faire des Tests en live il faudrait attendre \u00e0 chaque fois un weekend de Grand Prix et le faire en plus du commentaire que je dois produire. Pour toutes ces raisons et bien d'autres je l'ai rang\u00e9e dans la case \"Trop dur, Trop chiant, S\u00fbrement ill\u00e9gal\" (Oui je sais c'est une cat\u00e9gorie bien sp\u00e9cifique mais c'est ma documentation je fais ce que je veux) La troisi\u00e8me option aurait \u00e9t\u00e9 la plus simple (et moins dr\u00f4le) et je suis presque s\u00fbr que je peux impl\u00e9menter cette derni\u00e8re en moins d'une apr\u00e8s-midi. Sauf qu'elle apporte de gros soucis. On ne peux pas garantir l'int\u00e9grit\u00e9 et la continuit\u00e9 des donn\u00e9es si l'utilisateur avance ou fait pause m\u00eame par simple inadvertance. La moindre fen\u00eatre qui s'afficherait devant ruinerait toute la reconnaissance de caract\u00e8res. On ne peut pas contr\u00f4ler la qualit\u00e9 du flux et on est oblig\u00e9 de faire confiance en l'utilisateur On ne peut pas vraiment automatiser quoi que ce soit niveau tests ou m\u00eame pour faire du scrapping auto pour remplir une base de donn\u00e9e. Et finalement le pire inconv\u00e9nient : C'EST NUL ! Je ne pourrais jamais utiliser un projet qui fonctionne de cette facon, je ne peux pas me permettre d'avoir un \u00e9cran inutilisable quand je commente et auquel je dois constamment faire attention pour ne pas perturber la reconnaissance. Pour moi cette option aurait \u00e9t\u00e9 celle \u00e0 choisir en cas d'extr\u00eame urgence et en dernier recours car le projet deviendrait inutile. J'ai donc d\u00e9cid\u00e9 de m'occuper de la seconde option : Simuler un navigateur. Cette option bien que complexe et difficile \u00e0 impl\u00e9menter propose une solution \u00e0 tous les probl\u00eame et permet une r\u00e9cup\u00e8ration quasi sans compromis. Simuler un navigateur ? Simuler un navigateur internet n'est pas forc\u00e9ment tr\u00e8s difficile. Chromium par exemple offre une panoplie d'outils natifs et \u00e9norm\u00e9ment de librairies existent permettant de facilement et en quelques lignes simuler un Google Chrome et le contr\u00f4ler sans afficher son UI. \"Chromium logo\" {: style=\"height:150px;width:150px\"} Cependant. La F1TV n'utilise pas simplement un player HTML5 basique. Elle utilise un service de streaming BitMovin qui permet de fournir un stream de bonne qualit\u00e9 et surtout qui impl\u00e9mente les DRM (Digital Right Management) Cela veut dire que quand on ouvre un flux de la F1TV sur chrome et que l'on essaie de prendre une capture d'\u00e9cran, le player se met en noir et ne permet pas de voir quoi que ce soit (Certaines version de Chrome le permettent pendant quelques semaines avant de bloquer \u00e0 nouveau). Ce qui dans notre cas est un immense probl\u00e8me. Mais Firefox ne nous bloque pas de cette facon et il est donc assez facile de passer outre. L'explication sans trop rentrer dans les d\u00e9tails est la suivante : Dans chrome, le player par d\u00e9faut utilise une technologie appell\u00e9e \"PCP\" ou \"Protected Content Playback\" qui leur permet de bloquer au moins une partie des techniques de r\u00e9cup\u00e8ration du flux vid\u00e9o et audio. Cependant Firefox de pas sa nature Open Source utilise \"OpenH264\" pour lire ces m\u00eames flux soumis \u00e0 des DRM et OpenH264 n'impl\u00e9mente pas les m\u00eames restrictions. Sauf que Firefox n'est pas aussi facilement \u00e9mul\u00e9 que chrome et cela r\u00e9duit notre choix de librairies \u00e0 ... Une seule... Qui est Selenium. (Il existe aussi Pupetteer C# mais j'ai rencontr\u00e9 \u00e9norm\u00e9ment de soucis avec cette derni\u00e8re d\u00e8s que je voulais lancer une vid\u00e9o) \"Firefox dev logo\" {: style=\"height:150px;width:150px\"} Mais m\u00eame si la documentation est plut\u00f4t maigre parfois, c'est une bonne librairie qui permet de tr\u00e8s bien contr\u00f4ler une instance de chrome ou de Firefox. Contr\u00f4ler le navigateur Maintenant que l'on sait quel navigateur simuler et avec quelle technologie, on peut passer \u00e0 la r\u00e9alisation. Ce qu'il y a de bien avec Selenium, c'est qu'on a un certain nombre de commandes tr\u00e8s haut niveau qui nous permettent de contr\u00f4ler un navigateur de mani\u00e8re plut\u00f4t pr\u00e9cise. Je vais d\u00e9crire ici la proc\u00e9dure habituelle utilis\u00e9e sous une forme de recette de cuisine pour que l'on puisse facilement comprendre ce qu'il se passe. Durant cette explication je vais parler \u00e0 un moment de Cookies, ne vous en faites pas c'est le sous chapitre suivant qui va vous en parler. Recette de cuisine pour r\u00e9cup\u00e8rer des images de la F1TV : D\u00e9marrer une instance de navigateur avec les bons arguments Ajouter les bons param\u00eatres pour ne pas se faire flag comme un bot Naviguer sur la page de la F1TV Ajouter les cookies de connexion pour avoir acc\u00e8s au contenu de la page Naviguer sur la page du Grand Prix demand\u00e9 Attendre un peu que la page se charge Cliquer sur l'invite de cookies Attendre cinq secondes le temps que la page se reload Cliquer sur le bouton qui permet de passer du feed live \u00e0 la DATA CHANNEL Appuyer sur Espace pour faire apparaitre le bouton d'acc\u00e8s au param\u00eatres Cliquer sur le menu d\u00e9roulant des r\u00e9solution Trouver l'option 1080P et la selectionner Cliquer sur le bouton qui met la vid\u00e9o en plein \u00e9cran Prendre de screenshots \u00e0 intervales r\u00e9guliers Pour faire toutes ces actions on doit r\u00e9cup\u00e8rer les \u00e9l\u00e9ments selon leur ID ou leur classe. Voici un exemple qui r\u00e9cup\u00e8re le bouton de plein \u00e9cran et qui clique dessus : IWebElement fullScreenButton = Driver.FindElement(By.ClassName(\"bmpui-ui-fullscreentogglebutton\")); fullScreenButton.Click(); Ca peut para\u00eetre plut\u00f4t simple dit comme ca et quand tout fonctionne ca l'est mais la difficult\u00e9 vient du fait que \u00e0 peu pr\u00e8s nimporte laquelle de ces \u00e9tapes peut rater et qu'il faut donc faire un bon syst\u00e8me de gestion d'erreurs qui puisse aider l'utilisateur en cas de probl\u00e8me. Il faut dire aussi que les sites ne sont pas forc\u00e9ment tr\u00e8s content de voir des bots passer car cela peut \u00eatre un risque de DDOS et de Scraping (Comme moi) et donc ils mettent en place des syst\u00e8mes pour nous emp\u00eacher de faire ce que l'on veut On peut utiliser diff\u00e9rntes techniques pour passer outre ces restrictions comme : Changer son UserAgent Changer sa r\u00e9solution Ne pas avoir des patterns trop pr\u00e9visibles Avoir un historique Ne pas cliquer pile sur le milieu des boutons Ne pas cliquer trop vite Passer par un proxy pour ne pas se faire flag Utiliser des librairies plus discr\u00e8tes J'ai eu l'occasion de tester toutes ces methodes pour tenter de passer derri\u00e8re les radars de la F1TV et visiblement j'ai r\u00e9ussi pour les pages principales mais pas pour les pages de Login. Il faut savoir que la bataille entre bots et propri\u00e9taires de sites est un grand jeu du chat et de la souris et que les plateformes innovent constamment leur s\u00e9curit\u00e9. Et il se trouve que la partie login de la F1TV est heberg\u00e9e autre part que le reste du site chez Amazon et que elle poss\u00e8de les meilleures s\u00e9curit\u00e9s que j'aie pu voir. Aucunes des methodes que j'ai cit\u00e9es et d'autres encore que j'ai essay\u00e9 n'ont r\u00e9ussi \u00e0 fourvoyer le syst\u00e8me. J'ai donc \u00e9t\u00e9 oblig\u00e9 de faire appel \u00e0 la connexion par Cookies pour pouvoir acc\u00e8der au reste du site internet. R\u00e9cup\u00e8rer les cookies ? Alors, on va mettre de c\u00f4t\u00e9 toutes les questions de s\u00e9curit\u00e9 et de violation de la vie priv\u00e9e et de protection des donn\u00e9es des utilisateurs pour ce chapitre. Car pour faire simple, je siphonne TOUS les cookies de la persone qui utilise mon app. [FINIR CETTE EXPLICATIOn] Calibration [AJOUTER EXPLICATION] OCR Ici je vais parler de la seconde partie du projet qui parle du processus de reconnaissance de data sur une image du feed DATA de la F1TV. C'est je pense la partie qui a demand\u00e9 le plus tests et de refactor. Toute la partie OCR a \u00e9t\u00e9 d\u00e9velopp\u00e9e dans un projet \u00e0 part avant d'\u00eatre int\u00e9gr\u00e9e dans le projet final. Il faut savoir que la reconnaissance est diff\u00e9rente celon ce que l'on cherche. Je vais donc d\u00e9composer cette partie du document en sous rubriques selon les donn\u00e9es recherch\u00e9es. Mais avant ca je dois expliquer certains concepts qui seront importants. Fonctionnement g\u00e9n\u00e9ral Voici un screenshot de la page DATA de la F1TV que le programme va recevoir : \"Screen F1TV\" Si on regarde de loin on peut se dire que la structure est plut\u00f4t simple mais c'est loin d'\u00eatre le cas. On peut y voir au moins 4 zones contenant de l'information dans un format diff\u00e9rent. \"Zones principales\" Dans l'exemple ci dessus on peut voir 3 zones mais on aurait \u00e9galement pu comprendre la zone de position des pilotes autour du circuit pour faire 4. Ces 4 zones sont tr\u00e8s diff\u00e9rentes et contiennent d'autres informations. Pour ce travail de dipl\u00f4me je ne m'occupe que de la zone principale. Mais je pense que le titre et les infos de circuit ne prendrait pas tant de temps que ca \u00e0 impl\u00e9menter. J'ai utilis\u00e9 le mot \"Zone\" plus haut et ca n'est pas juste un mot utilis\u00e9 au hasard. C'est le nom de l'objet que j'utilise pour les repr\u00e9senter dans mon programme. Mais comme c'est important de bien comprendre ce concept avant de continuer je vais vous l'expliquer. ZONE : L'objet \"Zone\" parent est un objet qui est une zone d'image. Je m'explique, le but d'une zone est d'\u00eatre un morceau d'une image plus grande. Le but d'une Zone est de contenir une liste de plus petites Zones ou bien une liste de \"Window\" (j'explique ce que c'est juste apr\u00e8s). Elle contient la portion d'image qui la concerne et ses propres dimensions. Le parent zone ne pr\u00e9voit que de pouvoir ajouter ou supprimer des \u00e9l\u00e9ments des listes de zones ou de windows ainsi qu'une methode qui permet d'aller chercher toutes informations des livres qu'elle contient. L'int\u00e9r\u00eat d'une zone est de pouvoir compartimenter une image dans des parties int\u00e9ressantes au niveau de la reconnaissance mais pas de traiter d'information. WINDOW : L'objet \"Window\" est un objet qui peut ressembler beaucoup \u00e0 l'objet \"Zone\". En effet elle aussi est une partie d'une image plus grande et contient ses dimensions, mais elle se distingue en deux points importants. Elle ne contient pas d'autres Zones ou Windows Elle peut retourner les informations \u00e9crites sur son image. Toutes les Window qui h\u00e9ritent du parent Window peuvent impl\u00e9menter une methode qui permet de renvoyer ce qui peut \u00eatre d\u00e9cod\u00e9 sur son image. Les enfants peuvent aussi aller piocher dans les nombresues methodes de r\u00e9cup\u00e8ration de donn\u00e9es contenues dans le parent Window. Mieux vaut r\u00e9utiliser le plus possible que de r\u00e9inventer la roue pour chaque Window. Une analogie un peu bancale pourrait se pr\u00e9senter comme la suivante : La zone est une armoire ou une bibliot\u00e8que. Si c'est une zone qui contient d'autres zones c'est une bibliot\u00e8que et chacune de ces sous-zones sont des armoires. Leur unique but est de contenir de mani\u00e8re ordonn\u00e9e des objets qui eux contiennent de l'information. Les livres ici sont les Windows. Ils contiennet de l'information et sont stock\u00e9s dans des armoires et on y acc\u00e8de en allant dans la bonne bibliot\u00e8que et en allant dans la bonne armoire. Derni\u00e8res choses pour comprendre le diagramme: Il existe une Main Zone qui est une des 4 grandes zones dont je parlais dans la d\u00e9composition de l'image. Il existe aussi des \"Driver Zone\" qui sont de plus petites zones contenues dans la Main Zone qui et qui ne contiennent que les informations d'un pilote. L'objet Window n'est quasi jamais utilis\u00e9, c'est presque tout le temps des enfants de Window plus sp\u00e9cifiques qui sont utilis\u00e9s, le but est que chaque type d'information sur l'image aie son type de window. Voila donc un petit diagramme qui montre le d\u00e9coupage du programme : \"Diagramme explicatif de l'architecture des zones\" Pour visualiser encore un peu mieux comment ce d\u00e9coupage prend forme voici ce que chaque zone et Window contient. Main Zone : \"Exemple zone principale\" Driver Zone : \"Exemple zone de pilote\" Driver Position Window : \"Exemple de fen\u00eatre de position\" Driver name Window : \"Exemple de fen\u00eatre de nom\" Driver LapTime Window : \"Exemple de fen\u00eatre de temps au tour\" Driver Tyre Window : \"Exemple de fen\u00eatre pneus\" Il existe d'autres types de Window mais ce sont les principaux. On se rend assez facilement compte que chacunes de ces windows va avoir besoin d'un traitement sp\u00e9cifique car la mani\u00e8re de reconnaitre le pneu utilis\u00e9 et le temps au tour ne peut pas \u00eatre la m\u00eame. Pour r\u00e9sumer, on a un programme qui prend en entr\u00e9e un fichier de configuration, qui prend des images de la F1TV et les d\u00e9coupe dans des ZONES qui elles m\u00eame sont d\u00e9coup\u00e9es en WINDOWS pour qu'on puisse plus facilement les d\u00e9coder. Maintenant qu'on a une liste de diff\u00e9rent types de zones on peut commencer \u00e0 chercher ce qu'il y a marqu\u00e9 dessus. Pour cela il faut dabord comprendre un petit peu comment l'OCR fonctionne et comment des libraries comme Tesseract fonctionnent pour donner du texte en partant d'une image. Pour faire tr\u00e8s simple, nous avons un mod\u00e8le qui est entrain\u00e9. C'est \u00e0 dire que on donne \u00e0 un programme un tr\u00e8s grand nombre de mots ou de lettres en lui disant ce que contiennent chaques images. Ensuite le programme va cr\u00e9er des matrices de convolutions pour chaque lettre avec comme objectif de detecter les points communs entre les lettres pour cr\u00e9er un alpphabet. Par exemple la matric de la lettre 'H' donnerait un poids important \u00e0 des lignes verticales connect\u00e9es par une ligne centrale. Et si on fournis assez de donn\u00e9es de bonne qualit\u00e9 au mod\u00e8le, les matrices peuvent \u00eatre tr\u00e8s efficace \u00e0 detecter si une lettre est un H ou un M. Il y a pleins d'autres methodes comme l'utilisation d'un dictionnaire de mots de la langue pour permettre la reconnaissance de mots m\u00eame si une lettre au milieu n'est pas comprise ou en ajoutant d'autres informations sur le contexte mais ca ne nous int\u00e9resse pas ici. C'est important de comprendre comment cette reconnaissance de caract\u00e8res avec des matrices fonctionne car cela va nous aider \u00e0 pr\u00e9parer nos donn\u00e9es pour lui rendre la vie facile et augmenter la pr\u00e9cision de nos r\u00e9sultats. Filtres et traitement On peut essayer de donner toutes nos images directement \u00e0 Tesseract pour qu'il reconnaisse tout le texte qu'il y voit mais on risque de se retrouver avec des r\u00e9sultats au mieux inconsistents. Dans notre cas, le soucis est que les chiffres et lettres sont beaucoup trop petits. Ils ne font parfoisd que 10 pixels de haut et cela fait que il n'est pas forc\u00e9ment facile de toujours les diff\u00e9rencier. De plus, comme ils sont petits, les art\u00e9facts d'aliasing sont assez violents et peuvent grandement d\u00e9former une lettre ou un chiffre. Exemple : Prenons le chiffre 9. Dans l'image il peut \u00eatre repr\u00e9sent\u00e9 de cette mani\u00e8re : \"Exemple de chiffre avant post traitement\" On peut voir qu'il est flou, pour nous cela ne pose pas de probl\u00e8me et je pense que \u00e0 peu pr\u00e8s nimporte qui peut dire que c'est un 9. Cependant comme les contours sont flous et m\u00eame si on essaie de retirer le background : \"9 avec anti aliasing\" On voit que le 9 n'est pas clairement d\u00e9finit. En effet on pourrait le comprendre comme : \"Premier exemple de contours\" Ou comme : \"Second exemple de contours\" Voire m\u00eame simplement comme : \"Exemple de coutour g\u00e9n\u00e9reux\" Et on se rend bien compte que les performances de detection ne sont pas les m\u00eames dans ces trois cas. Il faut donc faire un certain post traitement des images pour supprimer les \u00e9l\u00e9ments parasites, les couleurs, et augmenter la visibilit\u00e9 des contours importants. Mais chaque type de donn\u00e9e va avoir des methodes de post traitement diff\u00e9rents. Donc voici les diff\u00e9rents types de reconnaissance et leur post traitements : Texte Alors ce type de reconnaissance est utilis\u00e9 par la WINDOW du nom de pilote et de la position du pilote. C'est je pense la plus simple de toutes car Tesseract est particuli\u00e8rement bien entrain\u00e9 pour. Cette reconnaissance concerne donc des lettres qui font des mots ou des noms. Voici un exemple de la WINDOW nom de pilote en entr\u00e9e : \"Exemple texte cru\" Ce texte peut paraitre bon, cependant quand on le lance dans Tesseract, il ne va pas toujours donner un r\u00e9sultat parfait. Il faut aussi savoir qu'il y a des noms pas mal plus p\u00e9nibles que Tesseract a plus de mal \u00e0 reconnaitres, soit \u00e0 cause des lettres utilis\u00e9es, soit car le nom est un nom d'une autre r\u00e9gion et qui ne veut rien dire en anglais ce qui emp\u00eache l'utilisation de dictionnaire (Ex : Tsunoda est un nom japonais et parfois il est difficile pour Tesseract de le reconnaitre car si une lettre pose probl\u00eame il ne peut pas trouver de contexte qui puisse l'aider). Donc pour le rendre plus facilement lisible et augmenter les chances que toutes les lettres soient d\u00e9couvertes, voici les \u00e9tapes que j'ai mis en place. 1 : J'inverse les couleurs. Je me suis rendu compte que il \u00e9tait souvent plus facile de trouver un noir sur blanc que blanc sur noir. Je ne suis pas sur que cette \u00e9tape soit capitale cependant. \"Texte invers\u00e9\" 2 : Je fais un Treshhold de 165 car avec moins le texte parfois prend trop du background et avec plus les lettres sont trop fines. \"Texte apr\u00e8s Treshold\" 3 : Je fais un Resize de l'image pour avoir une meilleure r\u00e9solution et permettre une meilleure d\u00e9tection. J'augmente la hauteur et la largeur par un facteur 2. J'ai trouv\u00e9 cette valeur suffisante et aller plus haut consomme beaucoup de ressources. \"Texte apr\u00e8s Resize\" 4: Je fais une tr\u00e8s rapide Dilatation du texte pour retirer le flou amen\u00e9 par la methode de Resize. Je n'utilise qu'une valeur de 1 car je ne veux pas trop changer comment le texte est model\u00e9 je veux juste retirer le flou. \"Texte apr\u00e8s Dilatation\" Explication des methodes pr\u00e9cises plus bas Voila pour ce qui est du post processing. Je ne dis pas que ce sont les meilleurs param\u00eatres possibles mais dans mes tests ce sont ceux qui ont le mieux march\u00e9s. C'est aussi les premi\u00e8res methodes que j'ai pu d\u00e9velopper alors forc\u00e9ment elles n'ont pas le niveau de d\u00e9tails de certaines autres. Mais comme m\u00eame avec ce traitement il n'est pas rare que je me retrouve avec une ou deux lettres pas justes, il faut un moyen d'\u00eatre s\u00fbr que c'est le bon nom qui est trouv\u00e9. Ce qu'il y a de pratique avec les noms de pilotes c'est que on sait d\u00e9ja comment ils s'appellent avant le Grand Prix. En effet dans le fichier de configuration de la reconnaissance, il y a une liste de noms de pilotes. Cela veut dire que au lieu de chercher \u00e0 trouver parfaitement les bonnes lettres, on peut simplement essayer de trouver quel nom de pilote ressemble le plus au nom trouv\u00e9 sur l'image. Pour ce faire j'ai utilis\u00e9 une methode appel\u00e9e la distance de Levenshtein. Pour faire simple c'est une methode qui va calculer les distances de lettres pour determiner entre des strings laquelle ressemble le plus \u00e0 une autre. Pour r\u00e9sumer le fonctionnement dans lordre : On prend l'image on la traite On envoie l'image trait\u00e9e \u00e0 Tesseract On trouve quel nom de pilote ressemble le plus \u00e0 ce r\u00e9sultat On renvoie le nom du pilote Chiffres Cette methode en r\u00e9alit\u00e9 utilise simplement la m\u00eame methode que celle qui va r\u00e9cup\u00e8rer le texte sur une image. Cependant, la, on envoie \u00e0 Tesseract l'information qu'il ne peut trouver que des chiffres sur l'image ce qui lui permet d'\u00eatre beaucoup plus pr\u00e9cis et de ne pas confondre un 9 avec un P ou un 11 avec un H PAR EXEMPLE (non pas que ca me soit arriv\u00e9 tr\u00e8s r\u00e9guli\u00e8rement et que ca me soit rest\u00e9 dans la gorge \u00e9videmment) L'avantage c'est que cette methode ne demande m\u00eame pas de traitement de la donn\u00e9e en sortie de Tesseract. On \u00e9sp\u00e8re simplement que le post traitement aura suffit. TEMPS : Cette methode regroupe la d\u00e9tection de temps au tour. Il y a trois grands types de WINDOW qui sont concern\u00e9es : La WINDOW du temps au tour La WINDOW du retard sur le leader La WINDOW des secteurs La grande diff\u00e9rence ce sont les ordres de grandeur. Les temps au tour sont en g\u00e9n\u00e9ral entre 50 secondes et 2 minutes. Tandis que les secteurs sont entre 20 et 30 secondes alors que le retard sur le leader peut-\u00eatre de plusieurs minutes. Cependant, tous ces temps poss\u00e8dent le m\u00eame type de post-traitement avant d'\u00eatre envoy\u00e9s \u00e0 Tesseract. Voici un exemple de temps au tour avant toute transformation : \"Temps au tour avant traitement\" On peut avoir l'impression que ce texte est tout \u00e0 fait lisible et facile \u00e0 d\u00e9coder surtout quand on le voit de loin comme ca. Cependant, il faut imaginer que ces chiffres font 13 pixels de haut en comptant le flou et comme expliqu\u00e9 plus haut ce flou dans ces echelles est terrible. \"Temps au tour zoom\u00e9\" Si on donne cette image \u00e0 Tesseract, les '3' deviennent des '9', des '9' deviennent des '8', des '2' deviennent eux aussi des '9', le tout parfois inversement et de mani\u00e8re compl\u00eatement impr\u00e9visible. Ca n'est simplement pas utilisable. Cette partie est un peu plus complexe car si la detection n'est pas fiable les chiffres sont simplement inutilisables. Si \u00e0 tout moment un temps au tour de 1:39.106 devient 1:32.108 c'est juste pas possible. Voici donc les \u00e9tapes de post-traitement que j'ai mis en place pour leur d\u00e9tection : 1: J'applique un Treshold de 185 pour enlever les ambiguit\u00e9s d'alisaising et avoir une image en noir et blanc claire. La valeur de 185 est assez \u00e9lev\u00e9e car le but est de vraiment garder uniquement les contours. Comme les chiffres se ressemlent beaucoup plu que les lettres, il faut tenter le plus possible de conserver leur formes sp\u00e9cifiques. Je me suis rendu compte que cette valeur \u00e9tait une de celles qui marchent le mieux. \"Temps au tour apr\u00e8s Treshold\" 2: J'applique un Resize de 2 pour augmenter la r\u00e9solution des chiffres et permettre une meilleure d\u00e9tection. Le but est d'avoir plus de pixels et donc de permettre \u00e0 Tesseract de mieux utiliser ses matrices de convolution. \"Temps au tour apr\u00e8s Resize\" 3: Comme le Resize am\u00e8ne du flou, j'utilise une methode de Dilatation qui me permet de retirer ce flou et de remplir un peu plus certaines parties qui ont \u00e9t\u00e9 un peu laiss\u00e9e par le Resize ; \"Temps au tour apr\u00e8s Dilatation\" 4: Contrairement aux mots plus haut, la rondeur ajout\u00e9e par la dilatation n'est pas vraiment d\u00e9sir\u00e9e. En effet, elle peut rendre confuse certains chiffres et emp\u00eacher Tesseract de bien trouver le chiffre. Alors j'applique une Erosion qui me permet de contrecarrer en partie les rondeurs ajout\u00e9es par la dilatation et retrouver des chiffres bien form\u00e9es. Pour l' Erosion et la Dilatation j'ai utilis\u00e9 une valeur de 1 car je ne voulais pas trop changer les chiffres. \"Temps au tour apr\u00e8s Erosion\" Explication des methodes pr\u00e9cises plus bas Et avec ce post processing on retrouve de plut\u00f4ts bon r\u00e9sultats qui demandent peu de traitement. Le traitement d\u00e9pend du type de WINDOW cependant. Pour les secteurs on indique \u00e0 Tesseract que les caract\u00e8res autoris\u00e9s sont : \"0123456789.\" Pour les temps au tour on autorise plut\u00f4t \"0123456789.:\" Et pour les \u00e9carts on autorise \"0123456789.+\" Ensuite on r\u00e9cup\u00e8re une liste de chiffres qui'il va falloir transformer en milisecondes pour faciliter le stockage et l'envoi. Le programme nettoie un peu la chaine avant de la convertir. Par exemple parfois le ':' de 1:34.456 est compris comme un '1' ou un '2' et il faut faire attention \u00e0 detecter quand ca arriver. Je passe les d\u00e9tails du reste du nettoyage car c'est vraiment du cas par cas mais quand on a finit de nettoyer la chaine on peut transformer les chaines de minutes secondes et milisecondes en un total de milisecondes. Pour r\u00e9sumer le fonctionnement dans l'ordre : On prend l'image et on lui applique une s\u00e9rie de filtres On envoie l'image filtr\u00e9e \u00e0 Tesseract On nettoie le r\u00e9sultat Tesseract pour compenser certains biais On convertis le r\u00e9sultat en milisecondes Pneus La on arrive sur la partie la plus p\u00e9nible. Pour comprendre la probl\u00e9matique il faut d'abord faire un petit point sur comment les pneus fonctionnent en Formule 1. Depuis 2019 en Formule 1 nous avons 5 grandes familles de pneus : Les pneus tendres Les pneus medium Les pneus durs Les pneus interm\u00e9diaires Les pneus pluie \"Gamme de pneus Pirelli\" Les trois premiers pneus sont des pneus faits pour piste s\u00e8che, le pneu interm\u00e9diaire pour piste humide et le neu pluie pour la pluie. Chaque pneu a sa dur\u00e9e de vie et son niveau de performance propre mais je ne vais pas rentrer dans le d\u00e9tail ici. Tout ce qu'il faut savoir ce que savoir sur quel pneu chaque pilote est et depuis combien de temps il les chausse est une information tr\u00e8s importante. Chaque pneu a une couleur donn\u00e9e qui permet de les diff\u00e9rencier. Voici un exemple de ce \u00e0 quoi une WINDOW de pneus peut ressembler : \"Exemple zone pneus 1\" Mais cette zone peut aussi ressembler \u00e0 ca : \"Exemple zone pneus 2\" Mais aussi \u00e0 ca : \"Exemple zone pneus 3\" Voire m\u00eame ca : \"Exemple zone pneus 4\" Je pense que vous pouvez tout de suite comprendre la difficult\u00e9 que repr\u00e9sente la t\u00e2che de r\u00e9cup\u00e8ration de donn\u00e9es \u00e0 partir de cette image. En gros le fonctionnement de cette zone d'information est assez simple. Au fur et \u00e0 mesure que la course avance, le trait fait de m\u00eame. Le chiffre dans le round tout \u00e0 droite indique le nombre de tour que le pilote a pass\u00e9 sur ce pneu. La couleur indique le type de pneu. Si il y a une lettre \u00e0 la place d'un chiffre c'est que c'est le premier tour sur ce pneu. La lettre indique le type de pneu. Et pas besoin de dire que si on essaie simplement de donner l'image \u00e0 Tesseract on ne r\u00e9cup\u00e8re ni les chiffres ni les lettres correctement si ce n'est pas du tout. Il faut donc utiliser une methode qui permette d'isoler le rond le plus \u00e0 droite, lui appliquer un traitement qui permette \u00e0 Tesseract de lire ce qu'il y a marqu\u00e9 et qui puisse determiner quel pneu est en train d'\u00eatre utilis\u00e9. J'ai d\u00e9cid\u00e9 de m'occuper dans un premier temps de trouver ce rond avant d'appliquer les filtres car plus l'image est petite plus les filtres sont rapides. Le programme va tirer un trait depuis le bord droit de la zone, et il va avancer vers la gauche jusqu'\u00e0 trouver un obstacle. Je d\u00e9tecte un obstacle si le pixel sur lequel est mon trait poss\u00e8de une valeur de plus de 0x50 dans le channel R,G ou B. J'ai trouv\u00e9 en faisant des tests que les couleurs de background de la F1TV ne d\u00e9passaient jamais ces valeurs. Ensuite apr\u00e8s avoir trouv\u00e9 le premier obstacle, je r\u00e9cup\u00e8re une zone qui doit englober le cercle. Voici un exemple avec cette image en entr\u00e9e : \"Zone compl\u00eate\" Elle est automatiquement coup\u00e9e de cette facon : \"Zone coup\u00e9e automatiquement\" Cela me permet d'isoler uniquement ce qui m'int\u00e9resse ce qui est tr\u00e8s pratique pour Tesseract et pour la detection de couleur. Ensuite avec cette image je peux commencer le processus de reconnaissance. Je commence par faire une moyenne de tous les pixels de l'image en excluant les pixels trop sombres qui font s\u00fbrement partie du background ou du chiffre. Ensuite j'utilise une methode qui calcule la diff\u00e9rence entre la couleur obbtenue et la liste de couleurs possible. Il y a cinq couleurs des pneus possibles : \"#ff0000\" pneu tendre/soft \"Couleur d'un pneu tendre\" \"#f5bf00\" pneu medium \"Couleur d'un pneu medium\" \"#a4a5a8\" pneu dur/hard \"Couleur d'un pneu dur\" \"#00a42e\" pneu inter \"Couleur d'un pneu interm\u00e9diaire\" \"#2760a6\" pneu pluie/wet \"Couleur d'un pneu pluie\" Ce qui est pratique c'est que m\u00eame dans les cas ou il n'y a pas beaucoup de couleur comme celui la : \"Pneu dur avec 0 tours\" On arrive \u00e0 une couleur moyenne de : \"Couleur moyenne de l'image ci dessus apr\u00e8s soustraction du background\" Et il est donc assez facile de determiner le type de pneu en question. Attention, les r\u00e9sultats peuvent \u00eatre tr\u00e8s vite d\u00e9rang\u00e9s par la couleur du pneu pr\u00e9c\u00e9dent si le d\u00e9coupage de la fen\u00eatre n'a pas \u00e9t\u00e9 assez pr\u00e9cis. Ensuite il \"suffit\" de lire le chiffre dans le rond et si on arrive pas \u00e0 le lire alors c'est que c'est une lettre et on sait que le nombre de tours est donc de 0. Maintenant vient le moment tr\u00e8s sympatique de la lecture du chiffre. Vous saurez que Tesseract en plus de detester les grandes images et les images avec des couleurs, deteste \u00e9galement les formes dans une image. Donc dans notre cas, le round de couleur autour du chiffre, m\u00eame si il n'est pas complet, il interf\u00e8re avec la reconnaissance et emp\u00eache de bien lire le chiffre. Il faut donc retirer le background et ensuite la couleur. Sauf que comme le chiffre est de la couleur du background, si on retire le background et ensuite la couleur il ne reste plus rien. Il faut donc retirer le background AUTOUR du rond, et ensuite si on retire la couleur il devrait rester le chiffre sur fond blanc. Pour se faire, j'ai tir\u00e9 des traits depuis les bords de l'image jusqu'\u00e0 ce qu'ils rencontrent le rond. Ensuite je retire tous les pixels entre le rond et les bords de l'image ce qui nous donne ceci : \"Zone pneu avec le background en moins\" Ensuite on peu retirer les pixels qui ont une valeur dans un channel RGB plus haute qu'un certain seuil : \"Zone avec le reste des couleurs supprimm\u00e9es\" Et la on a ce que l'on veut ! A partir de la c'est les filtres que l'on connait qui sont utilis\u00e9s pour en faire une image plus facile \u00e0 utiliser par Tesseract. 1 : On effectue un Resize de facteur 4 (oui c'est beaucoup mais en m\u00eame temps le chiffre est vraiment petit \u00e0 la base) qui permet d'avoir une image d'une bien meilleure r\u00e9solution. \"Filtre 1\" 2: On fait une Dilatation de facteur 1 pour retirer tout le flou de l'image pour aider Tesseract \"Resultat\" Et on a un chiffre qui est utilisable par Tesseract ! Explication des methodes pr\u00e9cises plus bas Pour r\u00e9sumer : On prend l'image de la zone et on la crop pour ne garder que la partie essentielle On d\u00e9termine le type de pneu avec la couleur moyenne de la zone On retire le background autour de cette zone On retire la couleur qui reste pour ne garder que le chiffre On augmente la r\u00e9solution du chiffre On rend ce chiffre net On envoie l'image trait\u00e9e et filtr\u00e9e \u00e0 Tesseract On d\u00e9termine le nombre de tours que le pilote a fait avec ses pneus avec le r\u00e9sultat de Tesseract DRS Bon ca c'\u00e9tait plut\u00f4t simple j'ai simplement v\u00e9rifi\u00e9 si la moyenne de vert d\u00e9passait une certaine valeur et puis voila. Filtres et methodes sur les images Dans ce projet on a du utiliser diff\u00e9rentes methodes d'\u00e9dition d'image que ce soit sous forme de filtres ou de modification de l'image directement. Voici un sommaire des methodes utilis\u00e9es et comment elles fonctionnent. Tresholding Cette methode sert \u00e0 passer d'une image en couleurs \u00e0 une image binaire noir blanc. C'est une \u00e9tape tr\u00e8s importante pour l'OCR car elle permet (si bien faite) d'isoler du texte de son background. Un exemple ici : \"Exemple treshold\" Le fonctionnement est assez simple mais il peut \u00eatre fait de diff\u00e9rentes mani\u00e8res mais dans mon cas voici comment l'algorythme fonctionne sachant qu'il demande en entr\u00e9e la Bitmap que l'on veut modifier ainsi que la valeur de Treshold : On parcours chaque pixel de l'image On convertir la couleur du pixel en une valeur de gris pour avoir la m\u00eame valeur en R,G et B (Formule utilis\u00e9e : grey = R x 0.3 + G x 0.59 + B x 0.11) Si le r\u00e9sultat de la valeur de gris est au dessus de la valeur de treshold, le pixel est pass\u00e9 en blanc complet et dans le cas contraire il est pass\u00e9 en noir complet On retourne la Bitmap modifi\u00e9e Un algorythme pas forc\u00e9ment complexe mais qui peut augmenter de mani\u00e8re titanesque les chances de r\u00e9ussir une OCR Resize Cette methode sert \u00e0 augmenter la r\u00e9solution d'une image pour am\u00e9liorer la pr\u00e9cision de l'algorythme de Tesseract. En effet, avec trop peu de pixels, la matrice de convolution n'est pas toujours aussi efficace. Il ne faut pas confondre cette methode d'augmentation de la taille avec une simple interpolation. En effet une augmentation de taille interpol\u00e9e ne vas pas vraiment changer la r\u00e9solution, l'image sera toujours aussi pixelis\u00e9e, seulement, les pixels seront compos\u00e9es de plus de pixels comme dans l'exemple ci dessous : \"Exemple d'interpolation lin\u00e9aire\" Dans mon projet j'utilise de l'interpolation bicubique qui va cr\u00e9er de l'information pour tenter de combler le vide et produire une image r\u00e9ellement plus grande et avec plus de details mais en ajoutant du flou. \"Exemple des diff\u00e9rents types d'interpolation\" Le but est d'aller chercher dans les pixels alentours les couleurs qui sont d\u00e9ja pr\u00e9sente et de jouer avec des poids pour tenter de faire une pr\u00e9diction de ce que ce pixel aurait \u00e9t\u00e9 si l'image avait plus de detail. Voici un exemple assez parlant : \"Exemple interpolation bicubique (avant)\" \"Exemple interpolation bicubique (apr\u00e8s)\" On pourrait croire que c'est inutile mais dans le contexte de Tesseract ajouter des d\u00e9tails pour tenter de simuler une meilleure r\u00e9solution m\u00eame en cr\u00e9ant du flou est int\u00e9ressant pour mieux remplir la matrice de convolution. Mais il est possible de r\u00e9duire ce flou avec d'autres m\u00e9thodes \u00e9galement. (Dans mon code je n'ai pas utilis\u00e9 du code fait main mais j'utilise une librairie qui me permet de le faire) Il faut simplement faire attention car c'est un proc\u00e9d\u00e9 assez lourd en performances. Dilatation et Erosion Cette methode et la suivante font partie des methodes de transformation morphologiques. Ces methodes sont utilis\u00e9es pour accentuer les formes et les epaissir ou les r\u00e9duire et les affiner. Elles poss\u00e8dent l'aventage \u00e9galement de retirer le flou d'une image ce qui est tr\u00e8s pratique si utilis\u00e9 apr\u00e8s l'utilisation de methodes comme Resize . Je ne vais pas trop rentrer dans les d\u00e9tails de ces methodes car leur fonctionnement est un peu plus lourd en math si on veut faire une v\u00e9ritable explication du pourquoi et du comment ca marche aussi bien. Pour notre projet je dirais que l'important est de savoir que ce sont deux outils tr\u00e8s pratiques pour changer la morphologie des lettres et des chiffres et qu'on peut les utiliser pour corriger du flou et/ou des art\u00e9facts apparus lors de la binarisation de l'image ou de la suppression de fond. Remove Background Cette methode est assez simple et est juste une methode qui va passer en revue tous les pixels de l'image et si la couleur d'un pixel s'apparente \u00e0 celle d'un pixel de fond il est pass\u00e9 en noir total ou en blanc total. Le but est de permettre au reste du programme de fonctionner avec des couleurs moins ambigues. Une variante sp\u00e9cialis\u00e9e pour la reconnaissance des pneus appel\u00e9e affectueusement Remove Useless cherche \u00e0 atteindre le m\u00eame bu mais est bien plus soffistiqu\u00e9e et sp\u00e9cialis\u00e9e pour retirer le background autour d'un cercle de couleur pour ensuite retirer la couleur et qu'il ne reste qu'un chiffre. Pour plus de details voir la detection de pneus. Il y aussi d'autre methodes comme un filtre Gaussien ou Highlight countour que j'ai du d\u00e9velopper mais que je n'ai pas utilis\u00e9 donc je ne vais pas en parler ici. Interpr\u00e9tation des donn\u00e9es Stockage des donn\u00e9es Affichage des donn\u00e9es Pr\u00e9dictions Tests [A remplir au fur et \u00e0 mesure de la cr\u00e9ation des tests] R\u00e9sum\u00e9 des difficult\u00e9s techniques [A remplir au fur et \u00e0 mesure dans la seconde moiti\u00e9 du travail de dipl\u00f4me] Optimisation du programme [A remplir \u00e0 la fin du projet pour parler des diff\u00e9rentes methodes d'optimisation] Ethique du projet [A remplir \u00e0 la fin du projet pour parler des questions ethiques du projet (Ex: Utilisation potentiellement abusive de la F1Tv ou L'histoire des cookies)] Am\u00e9liorations futures [A remplir dans les derni\u00e8res semaines du travail de dipl\u00f4me] Conclusion [A remplir la derni\u00e8re semaine du travail de dipl\u00f4me]","title":"Rapport Track Trends V1.0"},{"location":"index.html#rapport-track-trends-v10","text":"Rohmer Maxime Travail de dipl\u00f4me Technicien ES 2023","title":"Rapport Track Trends V1.0"},{"location":"index.html#introduction","text":"","title":"Introduction"},{"location":"index.html#resume","text":"Track Trends est un outil de r\u00e9cup\u00e9ration et d'analyse de donn\u00e9es de courses de Formule 1. Pour le contexte, en dehors des cours, j'exerce diff\u00e9rentes activit\u00e9s dont celle de Live Ticker F1 pour le 20 minutes. Pour m'aider dans ce travail, j'utilise actuellement la F1TV \u00e0 laquelle je suis abonn\u00e9 qui me propose non seulement un feed vid\u00e9o de meilleure qualit\u00e9 avec des commentaires plus pertinents que ceux de la RTS mais qui aussi me permet d'acc\u00e9der \u00e0 un feed vid\u00e9o tr\u00e8s important : la chaine data. Ce dernier ressemble \u00e0 cela : \"Screenshot du feed data de la f1tv\" (Attention ce n'est pas un joli tableau HTML, mais bien une vid\u00e9o qui contient un tableau.) Sauf que toutes les informations sont \u00e9tal\u00e9es p\u00eale-m\u00eale sans hi\u00e9rarchie ce qui fait que cela me prendrait trop de temps de tout d\u00e9chiffrer \u00e0 chaque fois, ce qui me fait rater des choses int\u00e9ressantes. Le but du projet est donc de fournir un outil qui hi\u00e9rarchise et affiche diff\u00e9remment les donn\u00e9es pour faciliter leur lecture et me permettre de faire de meilleurs commentaires.","title":"R\u00e9sum\u00e9"},{"location":"index.html#abstract","text":"Track Trends is a Formula 1 data is a tool that displays and interpret data. To understand everything,first ,a little bit of context. In my free time I have multiple activities and one is to be the Live Ticker F1 for the local journal \"20 minutes\" (Owned by Tamedia). to help me in this work I'm currently using the F1TV to which I'm currently subscribed because it provides me with a better video feed with better commentary than the ones from the RTS (in my opinion) but also because it gives me access to a very important video feed : the data channel You can see in the chapter above an example of the F1TV DATA CHANNEL. [Note : Even tough it looks like a pretty HTML table on wich you could easely get infos... Its not. Its a video feed] You can see a lot of data all well and good BUT! All the data is displayed the same in a big table which make it really hard to read totally in a hurry, which means that I miss a lot of useful information. The point of the project then is to provide a tool that can display those data by taking into account their relevance. So for example a driver that is 10s away from everyone and that is doing some normal lapTimes will be less displayed or even not displayed at all so I can focus on the drivers that are battling each others. This tool would help me not miss the battles and details that are happening in the back and therefore not being broadcasted on TV.And it could be a usefull tool for anyone who wants a better insight of how the race is going by looking at the data. This kind of project already exists in the form of the AWS tool \"F1 Insight\" but it is not avaible to the public. We can only see some of its predictions (that are trash) and data dumps in the live feed when the TV directors feel like it.","title":"Abstract"},{"location":"index.html#description-du-besoin","text":"Comme expliqu\u00e9 dans le r\u00e9sum\u00e9, je suis Live Ticker F1. Mais pour mieux comprendre le besoin que j'ai, je pense qu'il est pertinent de comprendre comment je travaille. Pendant un Grand Prix de Formule 1 j'ai plusieurs t\u00e2ches \u00e0 effectuer : Suivre les diff\u00e9rents \u00e9v\u00e8nements du Grand Prix Changer le titre et la photo de titre du Live Chercher des Tweets ou des Images \u00e0 int\u00e9grer Ecrire les commentaires en faisant attention \u00e0 dire ce qu'il se passe mais aussi l\u2019expliquer, ce que cela implique, mais aussi ce que cela veut dire pour le reste de la course. Comprendre et expliquer les strat\u00e9gies Tout cela toutes les cinq minutes max... Cela veut dire que je dois \u00eatre le plus rapide possible quand je cherche des informations. Et comme le tableau en comporte trop et bien, je suis oblig\u00e9 de le lire en diagonale. Par exemple dans le tableau, les infos que je cherche le plus sont : Le nombre de places gagn\u00e9es (surtout au d\u00e9part) Les \u00e9carts entre les pilotes (surtout ceux qui sont en dessous de deux secondes). Les pneus de chaque pilote et combien de tours, ils ont fait dessus Les temps d'arr\u00eats aux stands Les temps au tour (surtout pour la strat\u00e9gie) Mais pleins d'autres informations existent si on les recoupe sur plusieurs tours. Un outil qui permettrait de mettre en \u00e9vidence les informations importantes serait donc une tr\u00e8s grosse plus-value pour mon travail et s'il est facile \u00e0 installer et \u00e0 utiliser, il se pourrait qu'il devienne indispensable.","title":"Description du besoin"},{"location":"index.html#cahier-des-charges","text":"Il s'agit d'une version coup\u00e9e du cahier des charges qui ne contient pas l'explication du contexte. Mais l'original est disponible sur ce site \u00e9galement. Il est toutefois normal d'y voir des choses r\u00e9p\u00e9t\u00e9es ou l\u00e9g\u00e8rement diff\u00e9rentes, en effet, il n'a pas \u00e9t\u00e9 \u00e9crit en m\u00eame temps que le reste de ce document.","title":"Cahier des charges"},{"location":"index.html#projet","text":"Un outil de style compagnon sous forme d'application C# Windows Form qui r\u00e9cup\u00e8re en temps r\u00e9el les informations de la course et affiche les informations les plus importantes. Le but est non seulement de faciliter mon job, mais aussi faire en sorte d'am\u00e9liorer la plus-value de mon travail en me permettant de fournir des commentaires qui ne sont pas disponibles pour le tout venant qui regarde simplement le flux RTS. Exemples : Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand Maintenant afficher diff\u00e9remment les infos, c'est sympa, mais cela serait encore mieux de traiter ces data et de permettre des petites pr\u00e9dictions. Exemples : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents","title":"Projet"},{"location":"index.html#realisation","text":"Malheureusement, la Formula 1 Management ne propose aucune API publique qui puisse nous permettre de faire ce projet \"simplement\". La raison la plus probable \u00e9tant qu'Amazon avec son service AWS propose exactement ce genre de services pour le flux t\u00e9l\u00e9vis\u00e9 et il doit y avoir un contrat d'exclusivit\u00e9. Il existe des API \"Pirates\" faites par la communaut\u00e9, le probl\u00e8me est qu'elles ne sont pas forc\u00e9ment des plus pratiques \u00e0 utiliser. Mais comme je poss\u00e8de un abonnement premium ++ \u00e0 la F1TV, j'ai acc\u00e8s pour chaque grand prix \u00e0 un flux vid\u00e9o nomm\u00e9 : DATA F1 CHANNEL Qui ressemble \u00e0 \u00e7a : \"Exemple de la Data Channel\" Donc la seule fa\u00e7on que je vois de r\u00e9cup\u00e9rer ces donn\u00e9es est de les prendre directement sur ce feed. M\u00eame si le but final de l'application est de faire pleins de choses super avec les datas, le gros du projet va surtout \u00eatre la r\u00e9cup\u00e9ration des donn\u00e9es et leur stockage. Les donn\u00e9es viennent du flux vid\u00e9o et ainsi dans un premier temps, il va falloir r\u00e9cup\u00e9rer d'une mani\u00e8re ou d'une autre des images qui viennent d'un grand prix en direct ou en rediffusion. Ensuite, dans un second temps, il faut lire les informations directement sur l'image en utilisant une librairie pr\u00e9vue pour (exemple Tesseract) et v\u00e9rifier l'int\u00e9grit\u00e9 de ces derni\u00e8res pour qu'on puisse ensuite les stocker. Dans un troisi\u00e8me temps, il faut stocker toutes ces donn\u00e9es dans une forme qui permette d'aller facilement faire des requ\u00eates de r\u00e9cup\u00e9ration et d\u00e9j\u00e0 pr\u00e9parer des m\u00e9thodes qui permettent de r\u00e9cup\u00e9rer des infos importantes (ex : la moyenne des cinq derniers tours, le temps moyen d'arr\u00eat etc.) pour faciliter la derni\u00e8re \u00e9tape Quand tout cela est fait, on peut ensuite s'amuser un peu avec les Data. La derni\u00e8re \u00e9tape est donc l'affichage. L'id\u00e9e est de cr\u00e9er une Windows Form qui contienne toutes ces informations dans un format beaucoup plus lisible et avec laquelle on pourrait interagir pour permettre de plus facilement commenter les Grands Prix. (exemple plus bas avec un croquis de ce \u00e0 quoi l'application pourrait ressembler) Voici la liste des donn\u00e9es qui pourraient \u00eatre affich\u00e9es (Non contractuel, simplement des id\u00e9es). Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand La moyenne de temps que les pilotes perdent dans les stands La performance moyenne des 5 types de pneus La moyenne de temps de chaque pilote sur le pneu actuel Le nombre de points que chaque pilote gagnerait selon sa position Le classement de la course Voire m\u00eame si c'est possible : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents Pr\u00e9dire les temps au tour de chaque pilote selon l'usure des pneus Voici un exemple d'interface possible pour une page : \"Protype de l'app fait sur Figma\"","title":"R\u00e9alisation"},{"location":"index.html#cas-dutilisation","text":"'*'On va consid\u00e9rer que tous les user ont un abonnement F1 TV PRO Un user veut r\u00e9cup\u00e9rer les data : Il ouvre son navigateur et lance la page DATA de la F1 TV Il calibre la capture des data via le programme (pour la premi\u00e8re utilisation). Il confirme que les donn\u00e9es initiales sont bonnes (pour la premi\u00e8re utilisation). Il regarde tranquille son Grand Prix Le programme r\u00e9cup\u00e8re les data : Il r\u00e9cup\u00e8re des images depuis la F1TV Il utilise Tesseract (ou autre) pour en r\u00e9cup\u00e9rer les infos. Il met ces infos dans un Objet Pilote, dans un Objet course avec un attribut tour pour hi\u00e9rarchiser les data Pour ce qui est de l'affichage, l'id\u00e9e est de faire une application C# comme on l'a appris \u00e0 l'\u00e9cole, mais avec assez de style pour qu'elle puisse \u00eatre agr\u00e9able \u00e0 utiliser. Quand le programme affiche les data : Il prend les donn\u00e9es venant directement de la F1TV. Il affiche diff\u00e9remment les donn\u00e9es pour permettre une meilleure lisibilit\u00e9 Il interpr\u00e8te avec des r\u00e8gles plut\u00f4t simples certaines data pour faire des minipr\u00e9dictions ou aider \u00e0 la lecture Il r\u00e9cup\u00e8re des infos d'autres courses pour les comparer et proposer des pr\u00e9dictions plus int\u00e9ressantes","title":"Cas d'utilisation"},{"location":"index.html#difficultes-techniques","text":"R\u00e9cup\u00e9rer un flux vid\u00e9o plut\u00f4t propre malgr\u00e9 les contres mesures de la F1 TV pour en emp\u00eacher la lecture par un logiciel Si on doit passer par une capture d'\u00e9cran, trouver un moyen de stocker les donn\u00e9es de mani\u00e8re \u00e0 pr\u00e9voir que parfois un tour pourrait avoir plus de donn\u00e9es qu'un autre, que le user peut mettre pause, ou m\u00eame qu\u2019il revienne en arri\u00e8re. D\u00e9velopper des algorithmes pour r\u00e9cup\u00e9rer les donn\u00e9es comme les diff\u00e9rents pneus utilis\u00e9s ou l'activation du DRS ainsi que d\u00e9velopper des moyens de nettoyer les r\u00e9sultats de l'OCR (Par exemple utiliser diff\u00e9rents champs redondants pour comparer les r\u00e9sultats) Stocker les donn\u00e9es sur une base pour les traiter plus tard tout en pr\u00e9voyant un moyen de voir les stats live D\u00e9velopper des algorithmes de pr\u00e9diction qui prennent en compte d'anciennes courses pour tenter de pr\u00e9dire des choses comme les arr\u00eats aux stands par exemple.","title":"Difficult\u00e9s techniques"},{"location":"index.html#differences-sur-le-cahier-des-charges","text":"[\u00c0 remplir dans les derni\u00e8res semaines du travail de dipl\u00f4me]","title":"Diff\u00e9rences sur le cahier des charges"},{"location":"index.html#planning-previsionnel","text":"Mes suiveurs m'ont demand\u00e9 un planning de type GANTT pour ce travail de dipl\u00f4me Je n'ai pas utilis\u00e9 un logiciel particulier pour faire ce dernier, mais je me suis inspir\u00e9 des principes fondamentaux d'un diagramme de ce type. Comme l'original a \u00e9t\u00e9 fait sur Excel, je ne peux pas vraiment l'ins\u00e9rer de mani\u00e8re lisible ici, mais il est disponible dans le dossier Planning. Mais voici un r\u00e9sum\u00e9 de son contenu :","title":"Planning pr\u00e9visionnel"},{"location":"index.html#taches","text":"J'ai d\u00e9cid\u00e9 de d\u00e9composer mon planning en trois grands types de t\u00e2ches. Programmation Documentation Tests L'id\u00e9e est de permettre une meilleure lisibilit\u00e9 et de me permettre \u00e0 moi de me faire plus facilement \u00e0 l'id\u00e9e de ce qu'il m'attend. Voici la liste des t\u00e2ches par rubrique :","title":"T\u00e2ches"},{"location":"index.html#pt","text":"Cette rubrique contient les t\u00e2ches qui n'ont pas leur place dans les trois cat\u00e9gories principales.","title":"PT"},{"location":"index.html#pt1-preparation-au-travail-de-diplome-2","text":"Cette t\u00e2che est un peu hors cat\u00e9gorie, mais c'est normal, c'est une supert\u00e2che qui regroupe beaucoup de choses. C'est une t\u00e2che qui est planifi\u00e9e pour deux jours et qui normalement devrait \u00eatre faite les deux premiers jours du travail. Le but est de pr\u00e9parer tout ce qui peut \u00eatre pr\u00e9par\u00e9 en avance niveau documentation et mise en place pour ne pas avoir besoin de s'en soucier ensuite.","title":"PT1 / pr\u00e9paration au travail de dipl\u00f4me (2)"},{"location":"index.html#dt","text":"Rubrique documentation qui contient toutes les t\u00e2ches en rapport de pr\u00e8s ou de loin avec la documentation du projet.","title":"DT"},{"location":"index.html#dt1-creation-du-poster-1","text":"Cette t\u00e2che consiste \u00e0 faire une version num\u00e9rique du poster qui soit en accord avec les consignes qu'on nous a donn\u00e9es. Le but est aussi et surtout de faire poster dont je sois fier et que je sois content de montrer. Il y a d\u00e9j\u00e0 des croquis de poster et j'ai clairement pr\u00e9vu de travailler sur \u00e7a pendant les vacances alors, je n'ai mis qu'un jour et je l'ai plac\u00e9 juste avant le rendu de ce dernier.","title":"DT1 Cr\u00e9ation du poster (1)"},{"location":"index.html#dt2-documentation-analyse-de-lexistant-2","text":"Cette t\u00e2che est d\u00e9di\u00e9e \u00e0 l'\u00e9criture de la documentation et plus pr\u00e9cis\u00e9ment de l'analyse de l'existant. Comme il y a pas mal de technologies utilis\u00e9es dans mon projet, j'aimerais faire correctement un vrai debrief de pourquoi j'ai utilis\u00e9 l'une ou l'autre alors, j'ai assign\u00e9 deux jours dessus.","title":"DT2 Documentation Analyse de l'existant (2)"},{"location":"index.html#dt3-documentation-analyse-organique-5","text":"Cette t\u00e2che est la plus grosse dans la cat\u00e9gorie documentation. Il s'agit de documenter comment l'application fonctionne. J'y ai mis cinq jours et je pense que c'est un minimum car c'est dans cette t\u00e2che que je vais devoir d\u00e9tailler exactement comment fonctionne chaque partie du projet. Ces cinq jours sont \u00e9parpill\u00e9s sur le projet en g\u00e9n\u00e9ral \u00e0 la fin du d\u00e9veloppement de chaque grande partie de projet. Le but est de ne rien oublier et de ne pas avoir \u00e0 tout faire en m\u00eame temps.","title":"DT3 Documentation Analyse organique (5)"},{"location":"index.html#dt4-documentation-analyse-fonctionnelle-2","text":"Cette t\u00e2che est d\u00e9j\u00e0 moins grosse, elle consiste \u00e0 documenter le fonctionnement de l'application et comment utiliser les composants que j'ai d\u00e9velopp\u00e9s. Je l'ai mis en fin de projet, car comme j'ai l'habitude de faire des analyses fonctionnelles plut\u00f4t pr\u00e9cises, le moindre changement dans l'UI peut tout rendre obsol\u00e8te. J'y ai mis deux jours, car j'aimerais correctement documenter avec de bonnes photos et sc\u00e9narios pour qu'on puisse voir toutes les possibilit\u00e9s de l'application.","title":"DT4 Documentation Analyse fonctionnelle (2)"},{"location":"index.html#dt5-documentation-tests-1","text":"Cette t\u00e2che est un peu plus petite qu'elle ne le devrait. Elle concerne la documentation des diff\u00e9rents tests. Je n'y ai mis qu'un seul jour, car en r\u00e9alit\u00e9 les diff\u00e9rentes t\u00e2ches de tests contiennent aussi beaucoup de documentation,","title":"DT5 Documentation Tests (1)"},{"location":"index.html#dt6-documentation-reste-2","text":"Cette t\u00e2che est une t\u00e2che un peu vague. Elle contient toutes les actions autres que j'aurai besoin de faire (Mise au propre, orthographe, g\u00e9n\u00e9ration de PDF ...). J'y ai mis deux jours pour avoir un peu de marge, car ce sont toujours des t\u00e2ches qui paraissent faciles, mais qui \u00e0 la fin prennent beaucoup de temps si on les fait bien.","title":"DT6 Documentation Reste (2)"},{"location":"index.html#pt_1","text":"Rubrique programmation qui contient toutes les t\u00e2ches qui touchent \u00e0 la programmation et au d\u00e9veloppement de l'application.","title":"PT"},{"location":"index.html#pt1-programmation-recuperation-des-images-3","text":"Cette t\u00e2che est estim\u00e9e \u00e0 seulement trois jours, il ne faut pas s'y m\u00e9prendre, c'est une des t\u00e2ches les plus dures et lourdes niveaux documentation en explications. Cependant, un POC (Proof Of Concept) assez avanc\u00e9 a d\u00e9j\u00e0 \u00e9t\u00e9 fait et donc cela permet de n'envisager que trois jours, car il suffit de l'impl\u00e9menter et de la paufinner. Cette t\u00e2che consiste \u00e0 prendre en entr\u00e9e un lien de Grand Prix et de sortir une image tous les x secondes de la page DATA. Cela peut sembler simple, mais pour le faire sans prendre d'espace d'\u00e9cran et ne demandant pas \u00e0 l'utilisateur de copier-coller quoi que ce soit o\u00f9 de donner ses identifiants F1TV c'est un challenge. Cela peut paraitre curieux alors de mettre cette t\u00e2che loin dans le planning m\u00eame si c'est la premi\u00e8re \u00e9tape du projet. Encore une fois cela s'explique avec le fait qu'il y a d\u00e9j\u00e0 un POC qui fonctionne \u00e0 peu pr\u00e8s et que donc pr\u00e9f\u00e8re commencer avec des t\u00e2ches plus incertaines dans le cas o\u00f9 elles prendraient plus de temps que pr\u00e9vu.","title":"PT1 Programmation r\u00e9cup\u00e9ration des images (3)"},{"location":"index.html#pt2-programmation-ocr-5","text":"Cette t\u00e2che consiste \u00e0 d\u00e9velopper la partie qui reconnait le texte sur les images. C'est tr\u00e8s certainement la t\u00e2che qui risque le plus de d\u00e9border car c'est celle qui est la plus complexe techniquement puisqu'elle demande non seulement la lecture sur image, mais aussi le d\u00e9veloppement d'algorithmes de traitement de cette donn\u00e9e pour \u00eatre s\u00fbr qu'elle a bien \u00e9t\u00e9 lue. J'y ai ainsi allou\u00e9 cinq jours, mais j'esp\u00e8re que j'arriverai \u00e0 gagner du temps sur les autres pour y allouer plus dans le planning effectif, car je suis convaincu que plus, on y passe du temps, meilleur sera le r\u00e9sultat.","title":"PT2 Programmation OCR (5)"},{"location":"index.html#pt3-programmation-stockage-et-modele-5","text":"Cette partie est moins technique, mais concerne le stockage des donn\u00e9es que nous retourne la lecture des images. Mais elle va demander de la r\u00e9flexion et de l'intelligence de programmation, car il faut que cette partie anticipe les besoins de la vue et pr\u00e9pare un terrain fertile qui ne demande pas un refactor quand on passera au d\u00e9veloppement de la vue. C'est pour cela que je lui ai aussi assign\u00e9 cinq jours de travail et elle doit absolument \u00eatre commenc\u00e9e apr\u00e8s la lecture.","title":"PT3 Programmation, stockage et mod\u00e8le (5)"},{"location":"index.html#pt4-programmation-vue-de-lapp-5","text":"Cette partie peut \u00eatre horrible comme tr\u00e8s facile, cela d\u00e9pend compl\u00e8tement de la qualit\u00e9 du travail avant. Si le mod\u00e8le est parfait et que les donn\u00e9es sont int\u00e8gres, cela devrait \u00eatre plut\u00f4t simple de les afficher de mani\u00e8re int\u00e9ressante. Cependant, cette partie d\u00e9bordera s\u00fbrement un peu, car tout le temps gagn\u00e9 avec de bonnes donn\u00e9es sera utilis\u00e9 pour tenter de faire de la pr\u00e9diction. Pour ces raisons, je lui ai assign\u00e9 \u00e9galement cinq jours de travail et elle doit absolument \u00eatre faite apr\u00e8s le mod\u00e8le.","title":"PT4 Programmation Vue de l'APP (5)"},{"location":"index.html#pt5-programmation-mise-en-commun-3","text":"Cette t\u00e2che est aussi un petit peu sp\u00e9ciale, car elle regroupe plusieurs choses. En gros, chaque partie de programmation sera s\u00fbrement assez ind\u00e9pendante et il faudra \u00e0 un moment faire un seul projet C# qui contient tout. Il est difficile d'estimer \u00e0 quel point cela va \u00eatre compliqu\u00e9 alors, j'ai \u00e9t\u00e9 conservateur et j'ai mis trois jours.","title":"PT5 Programmation mise en commun (3)"},{"location":"index.html#tt","text":"Cette rubrique contient les t\u00e2ches qui sont uniquement des tests. La plupart des t\u00e2ches de programmations contiennent d\u00e9j\u00e0 des tests, mais certaines demandent une attention particuli\u00e8re.","title":"TT"},{"location":"index.html#tt1-tests-ocr-2","text":"Cette t\u00e2che est une des t\u00e2ches les plus importantes. Son but est de faire un protocole de tests complet qui permette de comparer les diff\u00e9rents algorithmes de reconnaissance de texte. Je l'ai mise apr\u00e8s la reconnaissance, mais m\u00eame maintenant en \u00e9crivant ces lignes, je me dis que dans le planning effectif, elle sera faite pendant la t\u00e2che de programmation. En effet, comment savoir si mon tout nouvel algorithme est r\u00e9ellement mieux que le pr\u00e9c\u00e9dent. Je pr\u00e9vois deux jours, car je pense que faire le dataset va prendre beaucoup de temps, il faut pr\u00e9voir un certain nombre d'images et de texte qui pourront ensuite \u00eatre donn\u00e9es sous forme de tests. C'est certes une t\u00e2che de test, mais c'est aussi de la programmation.","title":"TT1 Tests OCR (2)"},{"location":"index.html#tt2-tests-finaux-2","text":"Cette t\u00e2che de tests est un peu vague, elle regroupe les diff\u00e9rents tests pour v\u00e9rifier que les donn\u00e9es sont bien affich\u00e9es correctement. Ce qui serait cool si j'ai du temps en fin de travail de dipl\u00f4me serait de faire un syst\u00e8me de test qui permet d'entrainer le programme \u00e0 mieux reconnaitre certaines choses comme des arr\u00eats aux stands si on lui donne les trois derni\u00e8res ann\u00e9es de grands Prix. J'ai mis une dur\u00e9e arbitraire de deux jours, mais je ne sais pas vraiment combien de temps cela va vraiment durer. Elle est \u00e9videmment \u00e0 effectuer une fois que tout est \u00e0 peu pr\u00e8s termin\u00e9.","title":"TT2 Tests finaux (2)"},{"location":"index.html#planning-effectif-et-differences","text":"[A remplir dans les derni\u00e8res semaines du travail de dipl\u00f4me]","title":"Planning effectif et diff\u00e9rences"},{"location":"index.html#analyse-fonctionnelle","text":"[A remplir au fur et \u00e0 mesure dans la seconde moiti\u00e9 du travail de dipl\u00f4me]","title":"Analyse fonctionnelle"},{"location":"index.html#analyse-organique","text":"[A remplir au fur et \u00e0 mesure que les features sont d\u00e9velopp\u00e9es]","title":"Analyse Organique"},{"location":"index.html#recuperation-des-images","text":"Voici la premi\u00e8re grande \u00e9tape du projet. Pour rappel, Amazon h\u00e9berge directement le site de la F1TV et poss\u00e8de les droits sur les donn\u00e9es de la F1. C'est sous le nom de AWS (le service d'h\u00e9bergement d'Amazon) que la firme apparait en tant que sponsor. On peut voir ce nom appara\u00eetre assez souvent quand on regarde un Grand Prix car comme ils ont la main-mise sur les donn\u00e9es ils peuvent ins\u00e8rer des bandeaux d'informations sur le flux public sur ce qu'il se passe voir m\u00eame faire des pr\u00e9dictions (Bien qu'un peu bancales) \"Exemple insertion AWS en GP\" Ce service s'appelle F1 Insights (Oui c'est un meilleur nom de projet que F1 Companion mais bon) et c'est, je pense, la raison pour laquelle on ne voit aucune API publique qui permette de correctement se renseigner en don\u00e9es en direct pendant un Grand Prix. Ils ont du d\u00e9gotter un juteux contrat pour s'occuper de toute l'infrastructure digitale de la F1 (du moins publique) en \u00e9change d'une exclusivit\u00e9 totale sur certaines choses comme les Data \"Exemple data d'AWS\" Evidemment je ne fais que conjecturer et ce que j'ai dit n'est pas \u00e0 prendre au pied de la lettre mais c'est une explication possible je pense de pourquoi il est si difficile de trouver des donn\u00e9es sur la F1 facilement en temps r\u00e9el. Il existe bien quelques API un peu bancales publiques, mais le probl\u00e8me c'est qu'elles ne sont vraiment pas suffisante et je ne peux pas leur faire confiance quand je commente. Ce qu'il m'aurait fallut c'est une API publique et officielle qui me permette d'\u00eatre sur que les donn\u00e9es sont les bonnes et qu'elles arrivent le plus vite possible. On pourrait croire que c'est impossible car cela n'existe pas comme je l'ai dit MAIS ! Ce n'est pas compl\u00eatement vrai. En effet depuis que je poss\u00e8de un abonnement \u00e0 la F1TV, il existe une source d'informations tr\u00e8s pr\u00e9cieuse qui m'aide \u00e9norm\u00e9ment dans mon quotidien de commentateur de Formule 1. La \"DATA CHANNEL\". La Data Channel est une page de la F1TV qui permet, pour chaque Grand Prix, de visualiser, sous la forme d'un flux vid\u00e9o, diff\u00e9rentes informations capitales sur la course. \"Exemple de Data Channel\" Le probl\u00e8me, c'est que comme je viens de le dire, ces donn\u00e9es ne sont pas accessibles comme un tableau HTML ou un flux RSS ou un tableau JSON. C'est un flux vid\u00e9o. Il faut savoir qu'entretenir une diffusion de flux vid\u00e9o en 1080P pendant deux heures accessible par des milliers d'abonn\u00e9s est EXTR\u00caMENT cher surtout quand on le compare \u00e0 simplement afficher les donn\u00e9es dans un tableau. Ce qui veut dire que ce choix est d\u00e9lib\u00e9r\u00e9 et a un sens au niveau \u00e9conomique. Je pense donc que c'est justement pour \u00e9viter que des petits malins puissent simplement venir scraper l'int\u00e9gralit\u00e9 des donn\u00e9es qu'ils proposent et fasse sa propre API. (C'est d'ailleurs un des sites avec la meilleure protection anti bot du monde) MAIS ce n'est pas par ce que les donn\u00e9es ne sont pas facile \u00e0 avoir qu'elles sont IMPOSSIBLE \u00e0 avoir. Et c'est la que ce projet entre en jeu. Mais pour d\u00e9coder les donn\u00e9es d'une image il faut dabord ... (roulement de tambours) ... Avoir des images ! Et c'est la que vient se glisser cette partie du projet.","title":"R\u00e9cup\u00e9ration des images"},{"location":"index.html#comment-faire","text":"Le but de ce segment est de se concentrer sur la r\u00e9cup\u00e8ration et la mise \u00e0 disposition pour le reste du programme, des images en direct de la F1TV dans la meilleure qualit\u00e9 possible et dans les meilleurs d\u00e9lais. Pour ce faire il y a plusieurs solutions : Reverse engeneer la F1TV pour acc\u00e8der directement au flux sans passer par la plateforme internet et pouvoir prendres images \u00e0 volont\u00e9. Avoir tout simplement une page de la F1TV ouverte sur un \u00e9cran et prendres des screenshots \u00e0 intervals r\u00e9guliers. Simuler un navigateur internet sans qu'il soit affich\u00e9 et le contr\u00f4ler automatiquement pour qu'il prenne des captures. La premi\u00e8re option aurait \u00e9t\u00e9 la plus \u00e9l\u00e9gante mais lors d'un POC que je tentais de r\u00e9aliser je me suis rendu compte que cela serait un peu trop compliqu\u00e9 et long \u00e0 faire. Sans compter le fait que les rediffusions de Grand Prix ne sont pas g\u00e8r\u00e9es de la m\u00eame mani\u00e8re que les diffusions en live. Et que pour faire des Tests en live il faudrait attendre \u00e0 chaque fois un weekend de Grand Prix et le faire en plus du commentaire que je dois produire. Pour toutes ces raisons et bien d'autres je l'ai rang\u00e9e dans la case \"Trop dur, Trop chiant, S\u00fbrement ill\u00e9gal\" (Oui je sais c'est une cat\u00e9gorie bien sp\u00e9cifique mais c'est ma documentation je fais ce que je veux) La troisi\u00e8me option aurait \u00e9t\u00e9 la plus simple (et moins dr\u00f4le) et je suis presque s\u00fbr que je peux impl\u00e9menter cette derni\u00e8re en moins d'une apr\u00e8s-midi. Sauf qu'elle apporte de gros soucis. On ne peux pas garantir l'int\u00e9grit\u00e9 et la continuit\u00e9 des donn\u00e9es si l'utilisateur avance ou fait pause m\u00eame par simple inadvertance. La moindre fen\u00eatre qui s'afficherait devant ruinerait toute la reconnaissance de caract\u00e8res. On ne peut pas contr\u00f4ler la qualit\u00e9 du flux et on est oblig\u00e9 de faire confiance en l'utilisateur On ne peut pas vraiment automatiser quoi que ce soit niveau tests ou m\u00eame pour faire du scrapping auto pour remplir une base de donn\u00e9e. Et finalement le pire inconv\u00e9nient : C'EST NUL ! Je ne pourrais jamais utiliser un projet qui fonctionne de cette facon, je ne peux pas me permettre d'avoir un \u00e9cran inutilisable quand je commente et auquel je dois constamment faire attention pour ne pas perturber la reconnaissance. Pour moi cette option aurait \u00e9t\u00e9 celle \u00e0 choisir en cas d'extr\u00eame urgence et en dernier recours car le projet deviendrait inutile. J'ai donc d\u00e9cid\u00e9 de m'occuper de la seconde option : Simuler un navigateur. Cette option bien que complexe et difficile \u00e0 impl\u00e9menter propose une solution \u00e0 tous les probl\u00eame et permet une r\u00e9cup\u00e8ration quasi sans compromis.","title":"Comment faire ?"},{"location":"index.html#simuler-un-navigateur","text":"Simuler un navigateur internet n'est pas forc\u00e9ment tr\u00e8s difficile. Chromium par exemple offre une panoplie d'outils natifs et \u00e9norm\u00e9ment de librairies existent permettant de facilement et en quelques lignes simuler un Google Chrome et le contr\u00f4ler sans afficher son UI. \"Chromium logo\" {: style=\"height:150px;width:150px\"} Cependant. La F1TV n'utilise pas simplement un player HTML5 basique. Elle utilise un service de streaming BitMovin qui permet de fournir un stream de bonne qualit\u00e9 et surtout qui impl\u00e9mente les DRM (Digital Right Management) Cela veut dire que quand on ouvre un flux de la F1TV sur chrome et que l'on essaie de prendre une capture d'\u00e9cran, le player se met en noir et ne permet pas de voir quoi que ce soit (Certaines version de Chrome le permettent pendant quelques semaines avant de bloquer \u00e0 nouveau). Ce qui dans notre cas est un immense probl\u00e8me. Mais Firefox ne nous bloque pas de cette facon et il est donc assez facile de passer outre. L'explication sans trop rentrer dans les d\u00e9tails est la suivante : Dans chrome, le player par d\u00e9faut utilise une technologie appell\u00e9e \"PCP\" ou \"Protected Content Playback\" qui leur permet de bloquer au moins une partie des techniques de r\u00e9cup\u00e8ration du flux vid\u00e9o et audio. Cependant Firefox de pas sa nature Open Source utilise \"OpenH264\" pour lire ces m\u00eames flux soumis \u00e0 des DRM et OpenH264 n'impl\u00e9mente pas les m\u00eames restrictions. Sauf que Firefox n'est pas aussi facilement \u00e9mul\u00e9 que chrome et cela r\u00e9duit notre choix de librairies \u00e0 ... Une seule... Qui est Selenium. (Il existe aussi Pupetteer C# mais j'ai rencontr\u00e9 \u00e9norm\u00e9ment de soucis avec cette derni\u00e8re d\u00e8s que je voulais lancer une vid\u00e9o) \"Firefox dev logo\" {: style=\"height:150px;width:150px\"} Mais m\u00eame si la documentation est plut\u00f4t maigre parfois, c'est une bonne librairie qui permet de tr\u00e8s bien contr\u00f4ler une instance de chrome ou de Firefox.","title":"Simuler un navigateur ?"},{"location":"index.html#controler-le-navigateur","text":"Maintenant que l'on sait quel navigateur simuler et avec quelle technologie, on peut passer \u00e0 la r\u00e9alisation. Ce qu'il y a de bien avec Selenium, c'est qu'on a un certain nombre de commandes tr\u00e8s haut niveau qui nous permettent de contr\u00f4ler un navigateur de mani\u00e8re plut\u00f4t pr\u00e9cise. Je vais d\u00e9crire ici la proc\u00e9dure habituelle utilis\u00e9e sous une forme de recette de cuisine pour que l'on puisse facilement comprendre ce qu'il se passe. Durant cette explication je vais parler \u00e0 un moment de Cookies, ne vous en faites pas c'est le sous chapitre suivant qui va vous en parler. Recette de cuisine pour r\u00e9cup\u00e8rer des images de la F1TV : D\u00e9marrer une instance de navigateur avec les bons arguments Ajouter les bons param\u00eatres pour ne pas se faire flag comme un bot Naviguer sur la page de la F1TV Ajouter les cookies de connexion pour avoir acc\u00e8s au contenu de la page Naviguer sur la page du Grand Prix demand\u00e9 Attendre un peu que la page se charge Cliquer sur l'invite de cookies Attendre cinq secondes le temps que la page se reload Cliquer sur le bouton qui permet de passer du feed live \u00e0 la DATA CHANNEL Appuyer sur Espace pour faire apparaitre le bouton d'acc\u00e8s au param\u00eatres Cliquer sur le menu d\u00e9roulant des r\u00e9solution Trouver l'option 1080P et la selectionner Cliquer sur le bouton qui met la vid\u00e9o en plein \u00e9cran Prendre de screenshots \u00e0 intervales r\u00e9guliers Pour faire toutes ces actions on doit r\u00e9cup\u00e8rer les \u00e9l\u00e9ments selon leur ID ou leur classe. Voici un exemple qui r\u00e9cup\u00e8re le bouton de plein \u00e9cran et qui clique dessus : IWebElement fullScreenButton = Driver.FindElement(By.ClassName(\"bmpui-ui-fullscreentogglebutton\")); fullScreenButton.Click(); Ca peut para\u00eetre plut\u00f4t simple dit comme ca et quand tout fonctionne ca l'est mais la difficult\u00e9 vient du fait que \u00e0 peu pr\u00e8s nimporte laquelle de ces \u00e9tapes peut rater et qu'il faut donc faire un bon syst\u00e8me de gestion d'erreurs qui puisse aider l'utilisateur en cas de probl\u00e8me. Il faut dire aussi que les sites ne sont pas forc\u00e9ment tr\u00e8s content de voir des bots passer car cela peut \u00eatre un risque de DDOS et de Scraping (Comme moi) et donc ils mettent en place des syst\u00e8mes pour nous emp\u00eacher de faire ce que l'on veut On peut utiliser diff\u00e9rntes techniques pour passer outre ces restrictions comme : Changer son UserAgent Changer sa r\u00e9solution Ne pas avoir des patterns trop pr\u00e9visibles Avoir un historique Ne pas cliquer pile sur le milieu des boutons Ne pas cliquer trop vite Passer par un proxy pour ne pas se faire flag Utiliser des librairies plus discr\u00e8tes J'ai eu l'occasion de tester toutes ces methodes pour tenter de passer derri\u00e8re les radars de la F1TV et visiblement j'ai r\u00e9ussi pour les pages principales mais pas pour les pages de Login. Il faut savoir que la bataille entre bots et propri\u00e9taires de sites est un grand jeu du chat et de la souris et que les plateformes innovent constamment leur s\u00e9curit\u00e9. Et il se trouve que la partie login de la F1TV est heberg\u00e9e autre part que le reste du site chez Amazon et que elle poss\u00e8de les meilleures s\u00e9curit\u00e9s que j'aie pu voir. Aucunes des methodes que j'ai cit\u00e9es et d'autres encore que j'ai essay\u00e9 n'ont r\u00e9ussi \u00e0 fourvoyer le syst\u00e8me. J'ai donc \u00e9t\u00e9 oblig\u00e9 de faire appel \u00e0 la connexion par Cookies pour pouvoir acc\u00e8der au reste du site internet.","title":"Contr\u00f4ler le navigateur"},{"location":"index.html#recuperer-les-cookies","text":"Alors, on va mettre de c\u00f4t\u00e9 toutes les questions de s\u00e9curit\u00e9 et de violation de la vie priv\u00e9e et de protection des donn\u00e9es des utilisateurs pour ce chapitre. Car pour faire simple, je siphonne TOUS les cookies de la persone qui utilise mon app. [FINIR CETTE EXPLICATIOn]","title":"R\u00e9cup\u00e8rer les cookies ?"},{"location":"index.html#calibration","text":"[AJOUTER EXPLICATION]","title":"Calibration"},{"location":"index.html#ocr","text":"Ici je vais parler de la seconde partie du projet qui parle du processus de reconnaissance de data sur une image du feed DATA de la F1TV. C'est je pense la partie qui a demand\u00e9 le plus tests et de refactor. Toute la partie OCR a \u00e9t\u00e9 d\u00e9velopp\u00e9e dans un projet \u00e0 part avant d'\u00eatre int\u00e9gr\u00e9e dans le projet final. Il faut savoir que la reconnaissance est diff\u00e9rente celon ce que l'on cherche. Je vais donc d\u00e9composer cette partie du document en sous rubriques selon les donn\u00e9es recherch\u00e9es. Mais avant ca je dois expliquer certains concepts qui seront importants.","title":"OCR"},{"location":"index.html#fonctionnement-general","text":"Voici un screenshot de la page DATA de la F1TV que le programme va recevoir : \"Screen F1TV\" Si on regarde de loin on peut se dire que la structure est plut\u00f4t simple mais c'est loin d'\u00eatre le cas. On peut y voir au moins 4 zones contenant de l'information dans un format diff\u00e9rent. \"Zones principales\" Dans l'exemple ci dessus on peut voir 3 zones mais on aurait \u00e9galement pu comprendre la zone de position des pilotes autour du circuit pour faire 4. Ces 4 zones sont tr\u00e8s diff\u00e9rentes et contiennent d'autres informations. Pour ce travail de dipl\u00f4me je ne m'occupe que de la zone principale. Mais je pense que le titre et les infos de circuit ne prendrait pas tant de temps que ca \u00e0 impl\u00e9menter. J'ai utilis\u00e9 le mot \"Zone\" plus haut et ca n'est pas juste un mot utilis\u00e9 au hasard. C'est le nom de l'objet que j'utilise pour les repr\u00e9senter dans mon programme. Mais comme c'est important de bien comprendre ce concept avant de continuer je vais vous l'expliquer. ZONE : L'objet \"Zone\" parent est un objet qui est une zone d'image. Je m'explique, le but d'une zone est d'\u00eatre un morceau d'une image plus grande. Le but d'une Zone est de contenir une liste de plus petites Zones ou bien une liste de \"Window\" (j'explique ce que c'est juste apr\u00e8s). Elle contient la portion d'image qui la concerne et ses propres dimensions. Le parent zone ne pr\u00e9voit que de pouvoir ajouter ou supprimer des \u00e9l\u00e9ments des listes de zones ou de windows ainsi qu'une methode qui permet d'aller chercher toutes informations des livres qu'elle contient. L'int\u00e9r\u00eat d'une zone est de pouvoir compartimenter une image dans des parties int\u00e9ressantes au niveau de la reconnaissance mais pas de traiter d'information. WINDOW : L'objet \"Window\" est un objet qui peut ressembler beaucoup \u00e0 l'objet \"Zone\". En effet elle aussi est une partie d'une image plus grande et contient ses dimensions, mais elle se distingue en deux points importants. Elle ne contient pas d'autres Zones ou Windows Elle peut retourner les informations \u00e9crites sur son image. Toutes les Window qui h\u00e9ritent du parent Window peuvent impl\u00e9menter une methode qui permet de renvoyer ce qui peut \u00eatre d\u00e9cod\u00e9 sur son image. Les enfants peuvent aussi aller piocher dans les nombresues methodes de r\u00e9cup\u00e8ration de donn\u00e9es contenues dans le parent Window. Mieux vaut r\u00e9utiliser le plus possible que de r\u00e9inventer la roue pour chaque Window. Une analogie un peu bancale pourrait se pr\u00e9senter comme la suivante : La zone est une armoire ou une bibliot\u00e8que. Si c'est une zone qui contient d'autres zones c'est une bibliot\u00e8que et chacune de ces sous-zones sont des armoires. Leur unique but est de contenir de mani\u00e8re ordonn\u00e9e des objets qui eux contiennent de l'information. Les livres ici sont les Windows. Ils contiennet de l'information et sont stock\u00e9s dans des armoires et on y acc\u00e8de en allant dans la bonne bibliot\u00e8que et en allant dans la bonne armoire. Derni\u00e8res choses pour comprendre le diagramme: Il existe une Main Zone qui est une des 4 grandes zones dont je parlais dans la d\u00e9composition de l'image. Il existe aussi des \"Driver Zone\" qui sont de plus petites zones contenues dans la Main Zone qui et qui ne contiennent que les informations d'un pilote. L'objet Window n'est quasi jamais utilis\u00e9, c'est presque tout le temps des enfants de Window plus sp\u00e9cifiques qui sont utilis\u00e9s, le but est que chaque type d'information sur l'image aie son type de window. Voila donc un petit diagramme qui montre le d\u00e9coupage du programme : \"Diagramme explicatif de l'architecture des zones\" Pour visualiser encore un peu mieux comment ce d\u00e9coupage prend forme voici ce que chaque zone et Window contient. Main Zone : \"Exemple zone principale\" Driver Zone : \"Exemple zone de pilote\" Driver Position Window : \"Exemple de fen\u00eatre de position\" Driver name Window : \"Exemple de fen\u00eatre de nom\" Driver LapTime Window : \"Exemple de fen\u00eatre de temps au tour\" Driver Tyre Window : \"Exemple de fen\u00eatre pneus\" Il existe d'autres types de Window mais ce sont les principaux. On se rend assez facilement compte que chacunes de ces windows va avoir besoin d'un traitement sp\u00e9cifique car la mani\u00e8re de reconnaitre le pneu utilis\u00e9 et le temps au tour ne peut pas \u00eatre la m\u00eame. Pour r\u00e9sumer, on a un programme qui prend en entr\u00e9e un fichier de configuration, qui prend des images de la F1TV et les d\u00e9coupe dans des ZONES qui elles m\u00eame sont d\u00e9coup\u00e9es en WINDOWS pour qu'on puisse plus facilement les d\u00e9coder. Maintenant qu'on a une liste de diff\u00e9rent types de zones on peut commencer \u00e0 chercher ce qu'il y a marqu\u00e9 dessus. Pour cela il faut dabord comprendre un petit peu comment l'OCR fonctionne et comment des libraries comme Tesseract fonctionnent pour donner du texte en partant d'une image. Pour faire tr\u00e8s simple, nous avons un mod\u00e8le qui est entrain\u00e9. C'est \u00e0 dire que on donne \u00e0 un programme un tr\u00e8s grand nombre de mots ou de lettres en lui disant ce que contiennent chaques images. Ensuite le programme va cr\u00e9er des matrices de convolutions pour chaque lettre avec comme objectif de detecter les points communs entre les lettres pour cr\u00e9er un alpphabet. Par exemple la matric de la lettre 'H' donnerait un poids important \u00e0 des lignes verticales connect\u00e9es par une ligne centrale. Et si on fournis assez de donn\u00e9es de bonne qualit\u00e9 au mod\u00e8le, les matrices peuvent \u00eatre tr\u00e8s efficace \u00e0 detecter si une lettre est un H ou un M. Il y a pleins d'autres methodes comme l'utilisation d'un dictionnaire de mots de la langue pour permettre la reconnaissance de mots m\u00eame si une lettre au milieu n'est pas comprise ou en ajoutant d'autres informations sur le contexte mais ca ne nous int\u00e9resse pas ici. C'est important de comprendre comment cette reconnaissance de caract\u00e8res avec des matrices fonctionne car cela va nous aider \u00e0 pr\u00e9parer nos donn\u00e9es pour lui rendre la vie facile et augmenter la pr\u00e9cision de nos r\u00e9sultats.","title":"Fonctionnement g\u00e9n\u00e9ral"},{"location":"index.html#filtres-et-traitement","text":"On peut essayer de donner toutes nos images directement \u00e0 Tesseract pour qu'il reconnaisse tout le texte qu'il y voit mais on risque de se retrouver avec des r\u00e9sultats au mieux inconsistents. Dans notre cas, le soucis est que les chiffres et lettres sont beaucoup trop petits. Ils ne font parfoisd que 10 pixels de haut et cela fait que il n'est pas forc\u00e9ment facile de toujours les diff\u00e9rencier. De plus, comme ils sont petits, les art\u00e9facts d'aliasing sont assez violents et peuvent grandement d\u00e9former une lettre ou un chiffre. Exemple : Prenons le chiffre 9. Dans l'image il peut \u00eatre repr\u00e9sent\u00e9 de cette mani\u00e8re : \"Exemple de chiffre avant post traitement\" On peut voir qu'il est flou, pour nous cela ne pose pas de probl\u00e8me et je pense que \u00e0 peu pr\u00e8s nimporte qui peut dire que c'est un 9. Cependant comme les contours sont flous et m\u00eame si on essaie de retirer le background : \"9 avec anti aliasing\" On voit que le 9 n'est pas clairement d\u00e9finit. En effet on pourrait le comprendre comme : \"Premier exemple de contours\" Ou comme : \"Second exemple de contours\" Voire m\u00eame simplement comme : \"Exemple de coutour g\u00e9n\u00e9reux\" Et on se rend bien compte que les performances de detection ne sont pas les m\u00eames dans ces trois cas. Il faut donc faire un certain post traitement des images pour supprimer les \u00e9l\u00e9ments parasites, les couleurs, et augmenter la visibilit\u00e9 des contours importants. Mais chaque type de donn\u00e9e va avoir des methodes de post traitement diff\u00e9rents. Donc voici les diff\u00e9rents types de reconnaissance et leur post traitements :","title":"Filtres et traitement"},{"location":"index.html#texte","text":"Alors ce type de reconnaissance est utilis\u00e9 par la WINDOW du nom de pilote et de la position du pilote. C'est je pense la plus simple de toutes car Tesseract est particuli\u00e8rement bien entrain\u00e9 pour. Cette reconnaissance concerne donc des lettres qui font des mots ou des noms. Voici un exemple de la WINDOW nom de pilote en entr\u00e9e : \"Exemple texte cru\" Ce texte peut paraitre bon, cependant quand on le lance dans Tesseract, il ne va pas toujours donner un r\u00e9sultat parfait. Il faut aussi savoir qu'il y a des noms pas mal plus p\u00e9nibles que Tesseract a plus de mal \u00e0 reconnaitres, soit \u00e0 cause des lettres utilis\u00e9es, soit car le nom est un nom d'une autre r\u00e9gion et qui ne veut rien dire en anglais ce qui emp\u00eache l'utilisation de dictionnaire (Ex : Tsunoda est un nom japonais et parfois il est difficile pour Tesseract de le reconnaitre car si une lettre pose probl\u00eame il ne peut pas trouver de contexte qui puisse l'aider). Donc pour le rendre plus facilement lisible et augmenter les chances que toutes les lettres soient d\u00e9couvertes, voici les \u00e9tapes que j'ai mis en place. 1 : J'inverse les couleurs. Je me suis rendu compte que il \u00e9tait souvent plus facile de trouver un noir sur blanc que blanc sur noir. Je ne suis pas sur que cette \u00e9tape soit capitale cependant. \"Texte invers\u00e9\" 2 : Je fais un Treshhold de 165 car avec moins le texte parfois prend trop du background et avec plus les lettres sont trop fines. \"Texte apr\u00e8s Treshold\" 3 : Je fais un Resize de l'image pour avoir une meilleure r\u00e9solution et permettre une meilleure d\u00e9tection. J'augmente la hauteur et la largeur par un facteur 2. J'ai trouv\u00e9 cette valeur suffisante et aller plus haut consomme beaucoup de ressources. \"Texte apr\u00e8s Resize\" 4: Je fais une tr\u00e8s rapide Dilatation du texte pour retirer le flou amen\u00e9 par la methode de Resize. Je n'utilise qu'une valeur de 1 car je ne veux pas trop changer comment le texte est model\u00e9 je veux juste retirer le flou. \"Texte apr\u00e8s Dilatation\" Explication des methodes pr\u00e9cises plus bas Voila pour ce qui est du post processing. Je ne dis pas que ce sont les meilleurs param\u00eatres possibles mais dans mes tests ce sont ceux qui ont le mieux march\u00e9s. C'est aussi les premi\u00e8res methodes que j'ai pu d\u00e9velopper alors forc\u00e9ment elles n'ont pas le niveau de d\u00e9tails de certaines autres. Mais comme m\u00eame avec ce traitement il n'est pas rare que je me retrouve avec une ou deux lettres pas justes, il faut un moyen d'\u00eatre s\u00fbr que c'est le bon nom qui est trouv\u00e9. Ce qu'il y a de pratique avec les noms de pilotes c'est que on sait d\u00e9ja comment ils s'appellent avant le Grand Prix. En effet dans le fichier de configuration de la reconnaissance, il y a une liste de noms de pilotes. Cela veut dire que au lieu de chercher \u00e0 trouver parfaitement les bonnes lettres, on peut simplement essayer de trouver quel nom de pilote ressemble le plus au nom trouv\u00e9 sur l'image. Pour ce faire j'ai utilis\u00e9 une methode appel\u00e9e la distance de Levenshtein. Pour faire simple c'est une methode qui va calculer les distances de lettres pour determiner entre des strings laquelle ressemble le plus \u00e0 une autre. Pour r\u00e9sumer le fonctionnement dans lordre : On prend l'image on la traite On envoie l'image trait\u00e9e \u00e0 Tesseract On trouve quel nom de pilote ressemble le plus \u00e0 ce r\u00e9sultat On renvoie le nom du pilote","title":"Texte"},{"location":"index.html#chiffres","text":"Cette methode en r\u00e9alit\u00e9 utilise simplement la m\u00eame methode que celle qui va r\u00e9cup\u00e8rer le texte sur une image. Cependant, la, on envoie \u00e0 Tesseract l'information qu'il ne peut trouver que des chiffres sur l'image ce qui lui permet d'\u00eatre beaucoup plus pr\u00e9cis et de ne pas confondre un 9 avec un P ou un 11 avec un H PAR EXEMPLE (non pas que ca me soit arriv\u00e9 tr\u00e8s r\u00e9guli\u00e8rement et que ca me soit rest\u00e9 dans la gorge \u00e9videmment) L'avantage c'est que cette methode ne demande m\u00eame pas de traitement de la donn\u00e9e en sortie de Tesseract. On \u00e9sp\u00e8re simplement que le post traitement aura suffit. TEMPS : Cette methode regroupe la d\u00e9tection de temps au tour. Il y a trois grands types de WINDOW qui sont concern\u00e9es : La WINDOW du temps au tour La WINDOW du retard sur le leader La WINDOW des secteurs La grande diff\u00e9rence ce sont les ordres de grandeur. Les temps au tour sont en g\u00e9n\u00e9ral entre 50 secondes et 2 minutes. Tandis que les secteurs sont entre 20 et 30 secondes alors que le retard sur le leader peut-\u00eatre de plusieurs minutes. Cependant, tous ces temps poss\u00e8dent le m\u00eame type de post-traitement avant d'\u00eatre envoy\u00e9s \u00e0 Tesseract. Voici un exemple de temps au tour avant toute transformation : \"Temps au tour avant traitement\" On peut avoir l'impression que ce texte est tout \u00e0 fait lisible et facile \u00e0 d\u00e9coder surtout quand on le voit de loin comme ca. Cependant, il faut imaginer que ces chiffres font 13 pixels de haut en comptant le flou et comme expliqu\u00e9 plus haut ce flou dans ces echelles est terrible. \"Temps au tour zoom\u00e9\" Si on donne cette image \u00e0 Tesseract, les '3' deviennent des '9', des '9' deviennent des '8', des '2' deviennent eux aussi des '9', le tout parfois inversement et de mani\u00e8re compl\u00eatement impr\u00e9visible. Ca n'est simplement pas utilisable. Cette partie est un peu plus complexe car si la detection n'est pas fiable les chiffres sont simplement inutilisables. Si \u00e0 tout moment un temps au tour de 1:39.106 devient 1:32.108 c'est juste pas possible. Voici donc les \u00e9tapes de post-traitement que j'ai mis en place pour leur d\u00e9tection : 1: J'applique un Treshold de 185 pour enlever les ambiguit\u00e9s d'alisaising et avoir une image en noir et blanc claire. La valeur de 185 est assez \u00e9lev\u00e9e car le but est de vraiment garder uniquement les contours. Comme les chiffres se ressemlent beaucoup plu que les lettres, il faut tenter le plus possible de conserver leur formes sp\u00e9cifiques. Je me suis rendu compte que cette valeur \u00e9tait une de celles qui marchent le mieux. \"Temps au tour apr\u00e8s Treshold\" 2: J'applique un Resize de 2 pour augmenter la r\u00e9solution des chiffres et permettre une meilleure d\u00e9tection. Le but est d'avoir plus de pixels et donc de permettre \u00e0 Tesseract de mieux utiliser ses matrices de convolution. \"Temps au tour apr\u00e8s Resize\" 3: Comme le Resize am\u00e8ne du flou, j'utilise une methode de Dilatation qui me permet de retirer ce flou et de remplir un peu plus certaines parties qui ont \u00e9t\u00e9 un peu laiss\u00e9e par le Resize ; \"Temps au tour apr\u00e8s Dilatation\" 4: Contrairement aux mots plus haut, la rondeur ajout\u00e9e par la dilatation n'est pas vraiment d\u00e9sir\u00e9e. En effet, elle peut rendre confuse certains chiffres et emp\u00eacher Tesseract de bien trouver le chiffre. Alors j'applique une Erosion qui me permet de contrecarrer en partie les rondeurs ajout\u00e9es par la dilatation et retrouver des chiffres bien form\u00e9es. Pour l' Erosion et la Dilatation j'ai utilis\u00e9 une valeur de 1 car je ne voulais pas trop changer les chiffres. \"Temps au tour apr\u00e8s Erosion\" Explication des methodes pr\u00e9cises plus bas Et avec ce post processing on retrouve de plut\u00f4ts bon r\u00e9sultats qui demandent peu de traitement. Le traitement d\u00e9pend du type de WINDOW cependant. Pour les secteurs on indique \u00e0 Tesseract que les caract\u00e8res autoris\u00e9s sont : \"0123456789.\" Pour les temps au tour on autorise plut\u00f4t \"0123456789.:\" Et pour les \u00e9carts on autorise \"0123456789.+\" Ensuite on r\u00e9cup\u00e8re une liste de chiffres qui'il va falloir transformer en milisecondes pour faciliter le stockage et l'envoi. Le programme nettoie un peu la chaine avant de la convertir. Par exemple parfois le ':' de 1:34.456 est compris comme un '1' ou un '2' et il faut faire attention \u00e0 detecter quand ca arriver. Je passe les d\u00e9tails du reste du nettoyage car c'est vraiment du cas par cas mais quand on a finit de nettoyer la chaine on peut transformer les chaines de minutes secondes et milisecondes en un total de milisecondes. Pour r\u00e9sumer le fonctionnement dans l'ordre : On prend l'image et on lui applique une s\u00e9rie de filtres On envoie l'image filtr\u00e9e \u00e0 Tesseract On nettoie le r\u00e9sultat Tesseract pour compenser certains biais On convertis le r\u00e9sultat en milisecondes","title":"Chiffres"},{"location":"index.html#pneus","text":"La on arrive sur la partie la plus p\u00e9nible. Pour comprendre la probl\u00e9matique il faut d'abord faire un petit point sur comment les pneus fonctionnent en Formule 1. Depuis 2019 en Formule 1 nous avons 5 grandes familles de pneus : Les pneus tendres Les pneus medium Les pneus durs Les pneus interm\u00e9diaires Les pneus pluie \"Gamme de pneus Pirelli\" Les trois premiers pneus sont des pneus faits pour piste s\u00e8che, le pneu interm\u00e9diaire pour piste humide et le neu pluie pour la pluie. Chaque pneu a sa dur\u00e9e de vie et son niveau de performance propre mais je ne vais pas rentrer dans le d\u00e9tail ici. Tout ce qu'il faut savoir ce que savoir sur quel pneu chaque pilote est et depuis combien de temps il les chausse est une information tr\u00e8s importante. Chaque pneu a une couleur donn\u00e9e qui permet de les diff\u00e9rencier. Voici un exemple de ce \u00e0 quoi une WINDOW de pneus peut ressembler : \"Exemple zone pneus 1\" Mais cette zone peut aussi ressembler \u00e0 ca : \"Exemple zone pneus 2\" Mais aussi \u00e0 ca : \"Exemple zone pneus 3\" Voire m\u00eame ca : \"Exemple zone pneus 4\" Je pense que vous pouvez tout de suite comprendre la difficult\u00e9 que repr\u00e9sente la t\u00e2che de r\u00e9cup\u00e8ration de donn\u00e9es \u00e0 partir de cette image. En gros le fonctionnement de cette zone d'information est assez simple. Au fur et \u00e0 mesure que la course avance, le trait fait de m\u00eame. Le chiffre dans le round tout \u00e0 droite indique le nombre de tour que le pilote a pass\u00e9 sur ce pneu. La couleur indique le type de pneu. Si il y a une lettre \u00e0 la place d'un chiffre c'est que c'est le premier tour sur ce pneu. La lettre indique le type de pneu. Et pas besoin de dire que si on essaie simplement de donner l'image \u00e0 Tesseract on ne r\u00e9cup\u00e8re ni les chiffres ni les lettres correctement si ce n'est pas du tout. Il faut donc utiliser une methode qui permette d'isoler le rond le plus \u00e0 droite, lui appliquer un traitement qui permette \u00e0 Tesseract de lire ce qu'il y a marqu\u00e9 et qui puisse determiner quel pneu est en train d'\u00eatre utilis\u00e9. J'ai d\u00e9cid\u00e9 de m'occuper dans un premier temps de trouver ce rond avant d'appliquer les filtres car plus l'image est petite plus les filtres sont rapides. Le programme va tirer un trait depuis le bord droit de la zone, et il va avancer vers la gauche jusqu'\u00e0 trouver un obstacle. Je d\u00e9tecte un obstacle si le pixel sur lequel est mon trait poss\u00e8de une valeur de plus de 0x50 dans le channel R,G ou B. J'ai trouv\u00e9 en faisant des tests que les couleurs de background de la F1TV ne d\u00e9passaient jamais ces valeurs. Ensuite apr\u00e8s avoir trouv\u00e9 le premier obstacle, je r\u00e9cup\u00e8re une zone qui doit englober le cercle. Voici un exemple avec cette image en entr\u00e9e : \"Zone compl\u00eate\" Elle est automatiquement coup\u00e9e de cette facon : \"Zone coup\u00e9e automatiquement\" Cela me permet d'isoler uniquement ce qui m'int\u00e9resse ce qui est tr\u00e8s pratique pour Tesseract et pour la detection de couleur. Ensuite avec cette image je peux commencer le processus de reconnaissance. Je commence par faire une moyenne de tous les pixels de l'image en excluant les pixels trop sombres qui font s\u00fbrement partie du background ou du chiffre. Ensuite j'utilise une methode qui calcule la diff\u00e9rence entre la couleur obbtenue et la liste de couleurs possible. Il y a cinq couleurs des pneus possibles : \"#ff0000\" pneu tendre/soft \"Couleur d'un pneu tendre\" \"#f5bf00\" pneu medium \"Couleur d'un pneu medium\" \"#a4a5a8\" pneu dur/hard \"Couleur d'un pneu dur\" \"#00a42e\" pneu inter \"Couleur d'un pneu interm\u00e9diaire\" \"#2760a6\" pneu pluie/wet \"Couleur d'un pneu pluie\" Ce qui est pratique c'est que m\u00eame dans les cas ou il n'y a pas beaucoup de couleur comme celui la : \"Pneu dur avec 0 tours\" On arrive \u00e0 une couleur moyenne de : \"Couleur moyenne de l'image ci dessus apr\u00e8s soustraction du background\" Et il est donc assez facile de determiner le type de pneu en question. Attention, les r\u00e9sultats peuvent \u00eatre tr\u00e8s vite d\u00e9rang\u00e9s par la couleur du pneu pr\u00e9c\u00e9dent si le d\u00e9coupage de la fen\u00eatre n'a pas \u00e9t\u00e9 assez pr\u00e9cis. Ensuite il \"suffit\" de lire le chiffre dans le rond et si on arrive pas \u00e0 le lire alors c'est que c'est une lettre et on sait que le nombre de tours est donc de 0. Maintenant vient le moment tr\u00e8s sympatique de la lecture du chiffre. Vous saurez que Tesseract en plus de detester les grandes images et les images avec des couleurs, deteste \u00e9galement les formes dans une image. Donc dans notre cas, le round de couleur autour du chiffre, m\u00eame si il n'est pas complet, il interf\u00e8re avec la reconnaissance et emp\u00eache de bien lire le chiffre. Il faut donc retirer le background et ensuite la couleur. Sauf que comme le chiffre est de la couleur du background, si on retire le background et ensuite la couleur il ne reste plus rien. Il faut donc retirer le background AUTOUR du rond, et ensuite si on retire la couleur il devrait rester le chiffre sur fond blanc. Pour se faire, j'ai tir\u00e9 des traits depuis les bords de l'image jusqu'\u00e0 ce qu'ils rencontrent le rond. Ensuite je retire tous les pixels entre le rond et les bords de l'image ce qui nous donne ceci : \"Zone pneu avec le background en moins\" Ensuite on peu retirer les pixels qui ont une valeur dans un channel RGB plus haute qu'un certain seuil : \"Zone avec le reste des couleurs supprimm\u00e9es\" Et la on a ce que l'on veut ! A partir de la c'est les filtres que l'on connait qui sont utilis\u00e9s pour en faire une image plus facile \u00e0 utiliser par Tesseract. 1 : On effectue un Resize de facteur 4 (oui c'est beaucoup mais en m\u00eame temps le chiffre est vraiment petit \u00e0 la base) qui permet d'avoir une image d'une bien meilleure r\u00e9solution. \"Filtre 1\" 2: On fait une Dilatation de facteur 1 pour retirer tout le flou de l'image pour aider Tesseract \"Resultat\" Et on a un chiffre qui est utilisable par Tesseract ! Explication des methodes pr\u00e9cises plus bas Pour r\u00e9sumer : On prend l'image de la zone et on la crop pour ne garder que la partie essentielle On d\u00e9termine le type de pneu avec la couleur moyenne de la zone On retire le background autour de cette zone On retire la couleur qui reste pour ne garder que le chiffre On augmente la r\u00e9solution du chiffre On rend ce chiffre net On envoie l'image trait\u00e9e et filtr\u00e9e \u00e0 Tesseract On d\u00e9termine le nombre de tours que le pilote a fait avec ses pneus avec le r\u00e9sultat de Tesseract","title":"Pneus"},{"location":"index.html#drs","text":"Bon ca c'\u00e9tait plut\u00f4t simple j'ai simplement v\u00e9rifi\u00e9 si la moyenne de vert d\u00e9passait une certaine valeur et puis voila.","title":"DRS"},{"location":"index.html#filtres-et-methodes-sur-les-images","text":"Dans ce projet on a du utiliser diff\u00e9rentes methodes d'\u00e9dition d'image que ce soit sous forme de filtres ou de modification de l'image directement. Voici un sommaire des methodes utilis\u00e9es et comment elles fonctionnent. Tresholding Cette methode sert \u00e0 passer d'une image en couleurs \u00e0 une image binaire noir blanc. C'est une \u00e9tape tr\u00e8s importante pour l'OCR car elle permet (si bien faite) d'isoler du texte de son background. Un exemple ici : \"Exemple treshold\" Le fonctionnement est assez simple mais il peut \u00eatre fait de diff\u00e9rentes mani\u00e8res mais dans mon cas voici comment l'algorythme fonctionne sachant qu'il demande en entr\u00e9e la Bitmap que l'on veut modifier ainsi que la valeur de Treshold : On parcours chaque pixel de l'image On convertir la couleur du pixel en une valeur de gris pour avoir la m\u00eame valeur en R,G et B (Formule utilis\u00e9e : grey = R x 0.3 + G x 0.59 + B x 0.11) Si le r\u00e9sultat de la valeur de gris est au dessus de la valeur de treshold, le pixel est pass\u00e9 en blanc complet et dans le cas contraire il est pass\u00e9 en noir complet On retourne la Bitmap modifi\u00e9e Un algorythme pas forc\u00e9ment complexe mais qui peut augmenter de mani\u00e8re titanesque les chances de r\u00e9ussir une OCR Resize Cette methode sert \u00e0 augmenter la r\u00e9solution d'une image pour am\u00e9liorer la pr\u00e9cision de l'algorythme de Tesseract. En effet, avec trop peu de pixels, la matrice de convolution n'est pas toujours aussi efficace. Il ne faut pas confondre cette methode d'augmentation de la taille avec une simple interpolation. En effet une augmentation de taille interpol\u00e9e ne vas pas vraiment changer la r\u00e9solution, l'image sera toujours aussi pixelis\u00e9e, seulement, les pixels seront compos\u00e9es de plus de pixels comme dans l'exemple ci dessous : \"Exemple d'interpolation lin\u00e9aire\" Dans mon projet j'utilise de l'interpolation bicubique qui va cr\u00e9er de l'information pour tenter de combler le vide et produire une image r\u00e9ellement plus grande et avec plus de details mais en ajoutant du flou. \"Exemple des diff\u00e9rents types d'interpolation\" Le but est d'aller chercher dans les pixels alentours les couleurs qui sont d\u00e9ja pr\u00e9sente et de jouer avec des poids pour tenter de faire une pr\u00e9diction de ce que ce pixel aurait \u00e9t\u00e9 si l'image avait plus de detail. Voici un exemple assez parlant : \"Exemple interpolation bicubique (avant)\" \"Exemple interpolation bicubique (apr\u00e8s)\" On pourrait croire que c'est inutile mais dans le contexte de Tesseract ajouter des d\u00e9tails pour tenter de simuler une meilleure r\u00e9solution m\u00eame en cr\u00e9ant du flou est int\u00e9ressant pour mieux remplir la matrice de convolution. Mais il est possible de r\u00e9duire ce flou avec d'autres m\u00e9thodes \u00e9galement. (Dans mon code je n'ai pas utilis\u00e9 du code fait main mais j'utilise une librairie qui me permet de le faire) Il faut simplement faire attention car c'est un proc\u00e9d\u00e9 assez lourd en performances. Dilatation et Erosion Cette methode et la suivante font partie des methodes de transformation morphologiques. Ces methodes sont utilis\u00e9es pour accentuer les formes et les epaissir ou les r\u00e9duire et les affiner. Elles poss\u00e8dent l'aventage \u00e9galement de retirer le flou d'une image ce qui est tr\u00e8s pratique si utilis\u00e9 apr\u00e8s l'utilisation de methodes comme Resize . Je ne vais pas trop rentrer dans les d\u00e9tails de ces methodes car leur fonctionnement est un peu plus lourd en math si on veut faire une v\u00e9ritable explication du pourquoi et du comment ca marche aussi bien. Pour notre projet je dirais que l'important est de savoir que ce sont deux outils tr\u00e8s pratiques pour changer la morphologie des lettres et des chiffres et qu'on peut les utiliser pour corriger du flou et/ou des art\u00e9facts apparus lors de la binarisation de l'image ou de la suppression de fond. Remove Background Cette methode est assez simple et est juste une methode qui va passer en revue tous les pixels de l'image et si la couleur d'un pixel s'apparente \u00e0 celle d'un pixel de fond il est pass\u00e9 en noir total ou en blanc total. Le but est de permettre au reste du programme de fonctionner avec des couleurs moins ambigues. Une variante sp\u00e9cialis\u00e9e pour la reconnaissance des pneus appel\u00e9e affectueusement Remove Useless cherche \u00e0 atteindre le m\u00eame bu mais est bien plus soffistiqu\u00e9e et sp\u00e9cialis\u00e9e pour retirer le background autour d'un cercle de couleur pour ensuite retirer la couleur et qu'il ne reste qu'un chiffre. Pour plus de details voir la detection de pneus. Il y aussi d'autre methodes comme un filtre Gaussien ou Highlight countour que j'ai du d\u00e9velopper mais que je n'ai pas utilis\u00e9 donc je ne vais pas en parler ici.","title":"Filtres et methodes sur les images"},{"location":"index.html#interpretation-des-donnees","text":"","title":"Interpr\u00e9tation des donn\u00e9es"},{"location":"index.html#stockage-des-donnees","text":"","title":"Stockage des donn\u00e9es"},{"location":"index.html#affichage-des-donnees","text":"","title":"Affichage des donn\u00e9es"},{"location":"index.html#predictions","text":"","title":"Pr\u00e9dictions"},{"location":"index.html#tests","text":"[A remplir au fur et \u00e0 mesure de la cr\u00e9ation des tests]","title":"Tests"},{"location":"index.html#resume-des-difficultes-techniques","text":"[A remplir au fur et \u00e0 mesure dans la seconde moiti\u00e9 du travail de dipl\u00f4me]","title":"R\u00e9sum\u00e9 des difficult\u00e9s techniques"},{"location":"index.html#optimisation-du-programme","text":"[A remplir \u00e0 la fin du projet pour parler des diff\u00e9rentes methodes d'optimisation]","title":"Optimisation du programme"},{"location":"index.html#ethique-du-projet","text":"[A remplir \u00e0 la fin du projet pour parler des questions ethiques du projet (Ex: Utilisation potentiellement abusive de la F1Tv ou L'histoire des cookies)]","title":"Ethique du projet"},{"location":"index.html#ameliorations-futures","text":"[A remplir dans les derni\u00e8res semaines du travail de dipl\u00f4me]","title":"Am\u00e9liorations futures"},{"location":"index.html#conclusion","text":"[A remplir la derni\u00e8re semaine du travail de dipl\u00f4me]","title":"Conclusion"},{"location":"CahierDesCharges.html","text":"Cahier des charges Cahier des charges \"Track Trends\" Travail de dipl\u00f4me Maxime Rohmer 2023 Contexte Je suis le \"Live Ticker\" charg\u00e9 de la Formule 1 pour le 20 minutes. On peut traduire cela comme commentateur de F1, avec tout de m\u00eame l'importante subtilit\u00e9 que je ne commente pas avec la voix, mais avec le clavier. Mes commentaires sont sous la forme de commentaires \u00e9crits live qui s'ajoutent au fur et \u00e0 mesure de l'\u00e9v\u00e8nement. Par exemple : \"Tour 28/54, Hamilton a fini par s'arr\u00eater et chausser des gommes tendres 13 tours apr\u00e8s Verstappen. L'Anglais va voir plus de 15 secondes \u00e0 rattraper, mais les gommes neuves et plus tendres que son rival devraient lui permettre s'il ne se fait pas trop ralentir par le trafic\". En g\u00e9n\u00e9ral avec un peu plus d'infos quand m\u00eame et cela tous les 3-4 tours Voici quelques exemples de pr\u00e9c\u00e9dents commentaires (Conseil : il y a un bouton pour montrer le feed dans l'ordre chronologique) : \"Commentaire Grand Prix de Belgique 2022\" \"Commentaire du Grand Prix de Singapour 2022\" \"Exemple commentaires\" Pendant un Grand Prix, je dois constamment : \u00c9crire ce qu'il se passe dans le grand prix et expliquer les enjeux Chercher r\u00e9guli\u00e8rement des m\u00e9dias \u00e0 inclure pour diversifier mon live (Tweets, Images etc.) Changer le titre et la description du live en fonction de l'\u00e9volution du Grand prix Et accessoirement regarder le grand prix pour y comprendre quelque chose Avec tout \u00e7a, il est tr\u00e8s difficile de garder un \u0153il sur la page DATA de la F1TV qui fournit pourtant des informations pr\u00e9cieuses. Je me retrouve parfois par exemple \u00e0 ne pas parler de d\u00e9passements dans le peloton, car ils ne sont pas retransmis \u00e0 la t\u00e9l\u00e9 alors que c'est une information importante. Autre exemple, occasionnellement le classement ne refl\u00e8te pas les vraies positions des pilotes. Les arr\u00eats aux stands font que du coup des pilotes qui devraient \u00eatre 15\u00e8mes se retrouvent 8\u1d49 puisqu'ils ne sont pas encore arr\u00eat\u00e9s. Cela peut de temps en temps pr\u00eater \u00e0 confusion. Projet Un outil de style compagnon sous forme d'application C# Windows Form qui r\u00e9cup\u00e8re en temps r\u00e9el les informations de la course et affiche les informations les plus importantes. Le but est non seulement de faciliter mon job, mais aussi faire en sorte d'am\u00e9liorer la plus-value de mon travail en me permettant de fournir des commentaires qui ne sont pas disponibles pour le tout venant qui regarde simplement le flux RTS. Exemples: Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand Maintenant afficher diff\u00e9remment les infos, c'est sympa, mais cela serait encore mieux de traiter ces data et de permettre des petites pr\u00e9dictions. Exemples : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents R\u00e9alisation Malheureusement, la Formula 1 Management ne propose aucune API publique qui puisse nous permettre de faire ce projet \"simplement\". La raison la plus probable \u00e9tant qu'Amazon avec son service AWS propose exactement ce genre de services pour le flux t\u00e9l\u00e9vis\u00e9 et il doit y avoir un contrat d'exclusivit\u00e9. Il existe des API \"Pirates\" faites par la communaut\u00e9, le probl\u00e8me est qu'elles ne sont pas forc\u00e9ment des plus pratiques \u00e0 utiliser. Mais comme je poss\u00e8de un abonnement premium ++ \u00e0 la F1TV, j'ai acc\u00e8s pour chaque grand prix \u00e0 un flux vid\u00e9o nomm\u00e9 : DATA F1 CHANNEL Qui ressemble \u00e0 \u00e7a : \"Data channel exemple\" Donc la seule fa\u00e7on que je vois de r\u00e9cup\u00e9rer ces donn\u00e9es est de les prendre directement sur ce feed. M\u00eame si le but final de l'application est de faire pleins de choses super avec les datas, le gros du projet va surtout \u00eatre la r\u00e9cup\u00e9ration des donn\u00e9es et leur stockage. Les donn\u00e9es viennent du flux vid\u00e9o et ainsi dans un premier temps, il va falloir r\u00e9cup\u00e9rer d'une mani\u00e8re ou d'une autre des images qui viennent d'un grand prix en direct ou en rediffusion. Ensuite, dans un second temps, il faut lire les informations directement sur l'image en utilisant une librairie pr\u00e9vue pour (exemple Tesseract) et v\u00e9rifier l'int\u00e9grit\u00e9 de ces derni\u00e8res pour qu'on puisse ensuite les stocker. Dans un troisi\u00e8me temps, il faut stocker toutes ces donn\u00e9es dans une forme qui permette d'aller facilement faire des requ\u00eates de r\u00e9cup\u00e9ration et d\u00e9j\u00e0 pr\u00e9parer des m\u00e9thodes qui permettent de r\u00e9cup\u00e9rer des infos importantes (ex : la moyenne des cinq derniers tours, le temps moyen d'arr\u00eat etc.) pour faciliter la derni\u00e8re \u00e9tape Quand tout cela est fait, on peut ensuite s'amuser un peu avec les Data. La derni\u00e8re \u00e9tape est donc l'affichage. L'id\u00e9e est de cr\u00e9er une Windows Form qui contienne toutes ces informations dans un format beaucoup plus lisible et avec laquelle on pourrait interagir pour permettre de plus facilement commenter les Grands Prix. (exemple plus bas avec un croquis de ce \u00e0 quoi l'application pourrait ressembler) Voici la liste des donn\u00e9es qui pourraient \u00eatre affich\u00e9es (Non contractuel, simplement des id\u00e9es). Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand La moyenne de temps que les pilotes perdent dans les stands La performance moyenne des 5 types de pneus La moyenne de temps de chaque pilote sur le pneu actuel Le nombre de points que chaque pilote gagnerait selon sa position Le classement de la course Voire m\u00eame si c'est possible : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents Pr\u00e9dire les temps au tour de chaque pilote selon l'usure des pneus Voici un exemple d'interface possible pour une page : \"Proto\" Cas d'utilisation *On va consid\u00e9rer que tous les user ont un abonnement F1 TV PRO Un user veut r\u00e9cup\u00e9rer les data : Il ouvre son navigateur et lance la page DATA de la F1 TV Il calibre la capture des data via le programme (pour la premi\u00e8re utilisation). Il confirme que les donn\u00e9es initiales sont bonnes (pour la premi\u00e8re utilisation). Il regarde tranquille son Grand Prix Le programme r\u00e9cup\u00e8re les data : Il r\u00e9cup\u00e8re des images depuis la F1TV Il utilise Tesseract (ou autre) pour en r\u00e9cup\u00e9rer les infos. Il met ces infos dans un Objet Pilote, dans un Objet course avec un attribut tour pour hi\u00e9rarchiser les data Pour ce qui est de l'affichage, l'id\u00e9e est de faire une application C# comme on l'a appris \u00e0 l'\u00e9cole, mais avec assez de style pour qu'elle puisse \u00eatre agr\u00e9able \u00e0 utiliser. Quand le programme affiche les data : Il prend les donn\u00e9es venant directement de la F1TV. Il affiche diff\u00e9remment les donn\u00e9es pour permettre une meilleure lisibilit\u00e9 Il interpr\u00e8te avec des r\u00e8gles plut\u00f4t simples certaines data pour faire des minipr\u00e9dictions ou aider \u00e0 la lecture Il r\u00e9cup\u00e8re des infos d'autres courses pour les comparer et proposer des pr\u00e9dictions plus int\u00e9ressantes Difficult\u00e9s techniques R\u00e9cup\u00e9rer un flux vid\u00e9o plut\u00f4t propre malgr\u00e9 les contres mesures de la F1 TV pour en emp\u00eacher la lecture par un logiciel Si on doit passer par une capture d'\u00e9cran, trouver un moyen de stocker les donn\u00e9es de mani\u00e8re \u00e0 pr\u00e9voir que parfois un tour pourrait avoir plus de donn\u00e9es qu'un autre, que le user peut mettre pause, ou m\u00eame qu\u2019il revienne en arri\u00e8re. D\u00e9velopper des algorithmes pour r\u00e9cup\u00e9rer les donn\u00e9es comme les diff\u00e9rents pneus utilis\u00e9s ou l'activation du DRS ainsi que d\u00e9velopper des moyens de nettoyer les r\u00e9sultats de l'OCR (Par exemple utiliser diff\u00e9rents champs redondants pour comparer les r\u00e9sultats) Stocker les donn\u00e9es sur une base pour les traiter plus tard tout en pr\u00e9voyant un moyen de voir les stats live D\u00e9velopper des algorithmes de pr\u00e9diction qui prennent en compte d'anciennes courses pour tenter de pr\u00e9dire des choses comme les arr\u00eats aux stands par exemple.","title":"Cahier des charges"},{"location":"CahierDesCharges.html#cahier-des-charges","text":"Cahier des charges \"Track Trends\" Travail de dipl\u00f4me Maxime Rohmer 2023","title":"Cahier des charges"},{"location":"CahierDesCharges.html#contexte","text":"Je suis le \"Live Ticker\" charg\u00e9 de la Formule 1 pour le 20 minutes. On peut traduire cela comme commentateur de F1, avec tout de m\u00eame l'importante subtilit\u00e9 que je ne commente pas avec la voix, mais avec le clavier. Mes commentaires sont sous la forme de commentaires \u00e9crits live qui s'ajoutent au fur et \u00e0 mesure de l'\u00e9v\u00e8nement. Par exemple : \"Tour 28/54, Hamilton a fini par s'arr\u00eater et chausser des gommes tendres 13 tours apr\u00e8s Verstappen. L'Anglais va voir plus de 15 secondes \u00e0 rattraper, mais les gommes neuves et plus tendres que son rival devraient lui permettre s'il ne se fait pas trop ralentir par le trafic\". En g\u00e9n\u00e9ral avec un peu plus d'infos quand m\u00eame et cela tous les 3-4 tours Voici quelques exemples de pr\u00e9c\u00e9dents commentaires (Conseil : il y a un bouton pour montrer le feed dans l'ordre chronologique) : \"Commentaire Grand Prix de Belgique 2022\" \"Commentaire du Grand Prix de Singapour 2022\" \"Exemple commentaires\" Pendant un Grand Prix, je dois constamment : \u00c9crire ce qu'il se passe dans le grand prix et expliquer les enjeux Chercher r\u00e9guli\u00e8rement des m\u00e9dias \u00e0 inclure pour diversifier mon live (Tweets, Images etc.) Changer le titre et la description du live en fonction de l'\u00e9volution du Grand prix Et accessoirement regarder le grand prix pour y comprendre quelque chose Avec tout \u00e7a, il est tr\u00e8s difficile de garder un \u0153il sur la page DATA de la F1TV qui fournit pourtant des informations pr\u00e9cieuses. Je me retrouve parfois par exemple \u00e0 ne pas parler de d\u00e9passements dans le peloton, car ils ne sont pas retransmis \u00e0 la t\u00e9l\u00e9 alors que c'est une information importante. Autre exemple, occasionnellement le classement ne refl\u00e8te pas les vraies positions des pilotes. Les arr\u00eats aux stands font que du coup des pilotes qui devraient \u00eatre 15\u00e8mes se retrouvent 8\u1d49 puisqu'ils ne sont pas encore arr\u00eat\u00e9s. Cela peut de temps en temps pr\u00eater \u00e0 confusion.","title":"Contexte"},{"location":"CahierDesCharges.html#projet","text":"Un outil de style compagnon sous forme d'application C# Windows Form qui r\u00e9cup\u00e8re en temps r\u00e9el les informations de la course et affiche les informations les plus importantes. Le but est non seulement de faciliter mon job, mais aussi faire en sorte d'am\u00e9liorer la plus-value de mon travail en me permettant de fournir des commentaires qui ne sont pas disponibles pour le tout venant qui regarde simplement le flux RTS. Exemples: Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand Maintenant afficher diff\u00e9remment les infos, c'est sympa, mais cela serait encore mieux de traiter ces data et de permettre des petites pr\u00e9dictions. Exemples : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents","title":"Projet"},{"location":"CahierDesCharges.html#realisation","text":"Malheureusement, la Formula 1 Management ne propose aucune API publique qui puisse nous permettre de faire ce projet \"simplement\". La raison la plus probable \u00e9tant qu'Amazon avec son service AWS propose exactement ce genre de services pour le flux t\u00e9l\u00e9vis\u00e9 et il doit y avoir un contrat d'exclusivit\u00e9. Il existe des API \"Pirates\" faites par la communaut\u00e9, le probl\u00e8me est qu'elles ne sont pas forc\u00e9ment des plus pratiques \u00e0 utiliser. Mais comme je poss\u00e8de un abonnement premium ++ \u00e0 la F1TV, j'ai acc\u00e8s pour chaque grand prix \u00e0 un flux vid\u00e9o nomm\u00e9 : DATA F1 CHANNEL Qui ressemble \u00e0 \u00e7a : \"Data channel exemple\" Donc la seule fa\u00e7on que je vois de r\u00e9cup\u00e9rer ces donn\u00e9es est de les prendre directement sur ce feed. M\u00eame si le but final de l'application est de faire pleins de choses super avec les datas, le gros du projet va surtout \u00eatre la r\u00e9cup\u00e9ration des donn\u00e9es et leur stockage. Les donn\u00e9es viennent du flux vid\u00e9o et ainsi dans un premier temps, il va falloir r\u00e9cup\u00e9rer d'une mani\u00e8re ou d'une autre des images qui viennent d'un grand prix en direct ou en rediffusion. Ensuite, dans un second temps, il faut lire les informations directement sur l'image en utilisant une librairie pr\u00e9vue pour (exemple Tesseract) et v\u00e9rifier l'int\u00e9grit\u00e9 de ces derni\u00e8res pour qu'on puisse ensuite les stocker. Dans un troisi\u00e8me temps, il faut stocker toutes ces donn\u00e9es dans une forme qui permette d'aller facilement faire des requ\u00eates de r\u00e9cup\u00e9ration et d\u00e9j\u00e0 pr\u00e9parer des m\u00e9thodes qui permettent de r\u00e9cup\u00e9rer des infos importantes (ex : la moyenne des cinq derniers tours, le temps moyen d'arr\u00eat etc.) pour faciliter la derni\u00e8re \u00e9tape Quand tout cela est fait, on peut ensuite s'amuser un peu avec les Data. La derni\u00e8re \u00e9tape est donc l'affichage. L'id\u00e9e est de cr\u00e9er une Windows Form qui contienne toutes ces informations dans un format beaucoup plus lisible et avec laquelle on pourrait interagir pour permettre de plus facilement commenter les Grands Prix. (exemple plus bas avec un croquis de ce \u00e0 quoi l'application pourrait ressembler) Voici la liste des donn\u00e9es qui pourraient \u00eatre affich\u00e9es (Non contractuel, simplement des id\u00e9es). Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand La moyenne de temps que les pilotes perdent dans les stands La performance moyenne des 5 types de pneus La moyenne de temps de chaque pilote sur le pneu actuel Le nombre de points que chaque pilote gagnerait selon sa position Le classement de la course Voire m\u00eame si c'est possible : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents Pr\u00e9dire les temps au tour de chaque pilote selon l'usure des pneus Voici un exemple d'interface possible pour une page : \"Proto\"","title":"R\u00e9alisation"},{"location":"CahierDesCharges.html#cas-dutilisation","text":"*On va consid\u00e9rer que tous les user ont un abonnement F1 TV PRO Un user veut r\u00e9cup\u00e9rer les data : Il ouvre son navigateur et lance la page DATA de la F1 TV Il calibre la capture des data via le programme (pour la premi\u00e8re utilisation). Il confirme que les donn\u00e9es initiales sont bonnes (pour la premi\u00e8re utilisation). Il regarde tranquille son Grand Prix Le programme r\u00e9cup\u00e8re les data : Il r\u00e9cup\u00e8re des images depuis la F1TV Il utilise Tesseract (ou autre) pour en r\u00e9cup\u00e9rer les infos. Il met ces infos dans un Objet Pilote, dans un Objet course avec un attribut tour pour hi\u00e9rarchiser les data Pour ce qui est de l'affichage, l'id\u00e9e est de faire une application C# comme on l'a appris \u00e0 l'\u00e9cole, mais avec assez de style pour qu'elle puisse \u00eatre agr\u00e9able \u00e0 utiliser. Quand le programme affiche les data : Il prend les donn\u00e9es venant directement de la F1TV. Il affiche diff\u00e9remment les donn\u00e9es pour permettre une meilleure lisibilit\u00e9 Il interpr\u00e8te avec des r\u00e8gles plut\u00f4t simples certaines data pour faire des minipr\u00e9dictions ou aider \u00e0 la lecture Il r\u00e9cup\u00e8re des infos d'autres courses pour les comparer et proposer des pr\u00e9dictions plus int\u00e9ressantes","title":"Cas d'utilisation"},{"location":"CahierDesCharges.html#difficultes-techniques","text":"R\u00e9cup\u00e9rer un flux vid\u00e9o plut\u00f4t propre malgr\u00e9 les contres mesures de la F1 TV pour en emp\u00eacher la lecture par un logiciel Si on doit passer par une capture d'\u00e9cran, trouver un moyen de stocker les donn\u00e9es de mani\u00e8re \u00e0 pr\u00e9voir que parfois un tour pourrait avoir plus de donn\u00e9es qu'un autre, que le user peut mettre pause, ou m\u00eame qu\u2019il revienne en arri\u00e8re. D\u00e9velopper des algorithmes pour r\u00e9cup\u00e9rer les donn\u00e9es comme les diff\u00e9rents pneus utilis\u00e9s ou l'activation du DRS ainsi que d\u00e9velopper des moyens de nettoyer les r\u00e9sultats de l'OCR (Par exemple utiliser diff\u00e9rents champs redondants pour comparer les r\u00e9sultats) Stocker les donn\u00e9es sur une base pour les traiter plus tard tout en pr\u00e9voyant un moyen de voir les stats live D\u00e9velopper des algorithmes de pr\u00e9diction qui prennent en compte d'anciennes courses pour tenter de pr\u00e9dire des choses comme les arr\u00eats aux stands par exemple.","title":"Difficult\u00e9s techniques"},{"location":"jdb.html","text":"Journal de bord Mercredi 29 Mars 2023 Premier jour du travail de dipl\u00f4me. Nous avons eu un briefing de mr Garcia et nous avons pu commencer \u00e0 pr\u00e9parer le travail. Nous avons eu les diff\u00e9rents fichiers nescessaires \u00e0 la bonne r\u00e9alisation du projet et je me suis mis \u00e0 faire les fichiers nescessaires La premi\u00e8re chose a \u00e9t\u00e9 de faire ce mkdocs dans lequel j'ai mis un fichier yml plut\u00f4t standart qui risque de changer au fur et \u00e0 mesure. Voici le premier yml : site_name: Documentation Diplome theme: name: material palette: # Palette toggle for light mode - media: \"(prefers-color-scheme: light)\" scheme: default toggle: icon: material/brightness-7 name: Switch to dark mode # Palette toggle for dark mode - media: \"(prefers-color-scheme: dark)\" scheme: slate toggle: icon: material/brightness-4 name: Switch to light mode markdown_extensions: - attr_list - md_in_html plugins: - glightbox - with-pdf Voici un example de \u00e0 quoi ca ressemble en forme de site \"Exemple mkdocs\" Ensuite il m'a fallu faire une version plus \u00e0 jour de mon cahier des charges car je n'y avait pas touch\u00e9 depuis novembre. J'ai envoy\u00e9 un mail \u00e0 mes enseignants pour qu'ils puissent y jeter un oeuil pour \u00eatre s\u00fbr que je n'ai rien chang\u00e9 qui les d\u00e9rangent. Monsieur Jayr m'a demad\u00e9 \u00e0 l'occasion de lui faire un planning type Gantt alors je me suis mis \u00e0 la t\u00e2che. J'ai fait un planning pr\u00e9visionnel et une l\u00e9gende les deux sont dispo dans le dossier planning de ce repertoire. Ensuite je me suis mis \u00e0 tout mettre sur git. A commencer par ce repertoire Et c'est deja la fin de la journ\u00e9e ! Demain j'avance un peu sur la doc avec ce que je peux d\u00e9ja remplir et je finis de pr\u00e9parer ce dont j'ai besoin pour commencer \u00e0 coder. Jeudi 30 Mars 2023 Aujourd'hui selon le planning je dois me charger des dernirers pr\u00e9paratifs pour commencer correctement. J'ai fait expr\u00e8s de prenre du temps pour ca au d\u00e9but pour ne pas me cr\u00e9er de soucis plus loin pendant le travail. Je vais envoyer par mail le planning que j'ai fait \u00e0 mes suiveurs. Ensuite je vais m'attaquer au squelette de la docmentation. Je vais essayer de remplir tout ce que je peux remplir dans un premier temps car cela tout ca de fait pour plus tard quitte \u00e0 modifier quelques aspects au fur et \u00e0 mesure. J'ai aussi d\u00e9sactiv\u00e9 mkdocs with pdf par ce que les r\u00e9sultats ne sont vraiment pas ceux que j'attends et cela ralentis \u00e9norm\u00e9ment le d\u00e9ploiment. J'ai aussi rassembl\u00e9 mes croquis pour le poster : \"Croquis Poster 1\" \"Croquis Poster 2\" On peut voir que dans un premier temps j'ai tent\u00e9 de faire un poster un peu plus stylis\u00e9 et marketing. Cependant apr\u00e8s avoir discut\u00e9 avec Mr Garcia et diff\u00e9rents profs dont un de l'HEPIA et ils m'ont indiqu\u00e9 que ce qui \u00e9tait attendu \u00e9tait moins du marketing qu'un diagramme de fonctionnement. On peut voir sur les derniers posters que le cot\u00e9 technique ressort de plus en plus. Le but sera de faire une version encore plus technique ou on peut voir les diff\u00e9rents fonctionnements de l'application avec les technologies utilis\u00e9es. Le d\u00e9fi cela va \u00eatre de faire un joli poster qui soit en m\u00eame temps vendeur et en m\u00eame temps rempli techniquement. Oh et j'ai eu un probl\u00e8me ou mon calvier et ma souris ne voulaient d'un coup plus fonctionner. Dans mon cas c'\u00e9tait un probl\u00e8me de power management des ports. J'ai eu le soucis sur mon pc fixe \u00e0 la maison et sur mon pc portable \u00e9galement. En gros de ce que j'ai compris le soucis c'est que le pc croit que un port est trop solicit\u00e9 niveau puissance et du coup d\u00e9cide de couper l'alimentation du port USB. J'ai pu r\u00e8gler le soucis en allant dans le device manager sous universal bus controller sous power management et en d\u00e9cochant la case qui indique que windows peut d\u00e9sactiver ce port. Je ne conseille pas ce fix si vous avez des composants de mauvaise qualit\u00e9 car cela pourrait \u00eatre une vraie alerte cependant le fait que mes composants sont plut\u00f4t haut de gamme et le fait que mon clavier et ma souris le fassent en m\u00eame temps et que ils fonctionnaient tr\u00e8s bien depuis plus de 4 ans me font penser que c'est juste une nouvelle mise a jour de windows qui est p\u00e9nible. Demain je vais pouvoir commencer \u00e0 coder pour de bon. Vendredi 31/03/2023 Aujourd'hui on s'occupe de la PT2 qui est la programmation de la r\u00e9cup\u00e8ration des informations des images. Je vais tester IronOcr Source : https://www.c-sharpcorner.com/article/ocr-using-tesseract-in-C-Sharp/ Doc : https://ironsoftware.com/csharp/ocr/docs/ Examples : https://ironsoftware.com/csharp/ocr/examples/simple-csharp-ocr-tesseract/ Avant d'utiliser la librairie je me demande si je dois utiliser un peu de post processing pour aider \u00e0 la reconnaissance. Je peux soit utiliser l'image crop\u00e9e directement : \"Image non trait\u00e9e\" Soit avec un filtre pour passer en noir et blanc laxiste \"Image trait\u00e9e laxiste\" Soit avec un filtre pour passer en noir et blanc stricte \"Image trait\u00e9e stricte\" Il va falloir faire des tests avec tous les noms et les chiffres pour trouver le plus efficace. Bon malheureusment Iron OCR semblait \u00eatre une bonne alternative mais c'est une librairie priv\u00e9e qui demande une license pour \u00eatre utilis\u00e9e. Il va falloir trouver autre chose. En utilisant la librairie \"Tesseract\" qui existe on peut faire de la reconnaissance de texte avec un code plut\u00f4t simple : TesseractEngine engine = new TesseractEngine(tessDataFolder.FullName,\"eng\", EngineMode.Default); var tessImage = Pix.LoadFromMemory(ImageToByte(sample)); Page page = engine.Process(tessImage); string text = page.GetText(); Voici la methode ImageToByte : https://stackoverflow.com/questions/7350679/convert-a-bitmap-into-a-byte-array public static byte[] ImageToByte(Image img) { using (var stream = new MemoryStream()) { img.Save(stream, System.Drawing.Imaging.ImageFormat.Png); return stream.ToArray(); } } Voici le code pour traiter plusieurs textes sur une seule image : Page page = engine.Process(tessImage); // Get the iterator for the page layout using (var iter = page.GetIterator()) { // Loop over the elements of the page layout iter.Begin(); do { // Declare a Rect variable to hold the bounding box Rect boundingBox; // Get the bounding box for the current element if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox)) { g.DrawRectangle(Pens.Red,new Rectangle(boundingBox.X1,boundingBox.Y1,boundingBox.Width,boundingBox.Height)); } // Get the text for the current element var text = iter.GetText(PageIteratorLevel.Word); tbxResult.Text += text.ToUpper() + Environment.NewLine; } while (iter.Next(PageIteratorLevel.Word)); } Etonnament, avec plus de texte, des noms qui \u00e9taient autrefois mal reconnus sont parfaitement interpr\u00eat\u00e9s. Par exemple voici un exemple de reconnaisance de texte sur tous les pilotes : \"Screenshot de reconnaisance d'image complete\" On voit que le nom Leclerc est mal reconnu. Mais voici ce que cela donne quand on prend une image qui ne contient que le nom Leclerc : \"Screenshot de reconnaissance d'image crop\u00e9e\" On voit ici que le nom Leclerc est tr\u00e8s bien reconnu. Dans le premier exemple on peut voir que Tsunoda est reconnu comme \"Reticin\" ce qui n'est pas exactement pareil (lol) Et quand on isole le nom Tsunoda dans une image seule : \"Screenshot de reconnaissance de Tsunoda\" Il le lit \"RETLELYY\" ce qui n'est toujours pas exactement ca... Une meilleure r\u00e9solution pourrait peut-\u00eatre r\u00e9soudre le probl\u00e8me en partie. Jusqu'ici les images \u00e9taient en presque 720P ce qui donne ceci : \"Tsunoda en 720P\" Et j'ai lanc\u00e9 une r\u00e9cup\u00e8ration d'images en 1080p pour r\u00e9cup\u00e8rer ceci : \"Tsunoda en 1080P\" On peut voir une certaine diff\u00e9rence tout de m\u00eame. Et quand on lance la reconnaissance : \"Reconnaissance Tsunoda en 1080P\" \"Tsunoda n'est plus \u00e9crit \"RETLELYY\" mais \"TSUNDDA\" ce qui n'est pas parfait mais qui est d\u00e9ja beaucoup mieux. J'ai essay\u00e9 de mettre l'engine de Tesseract en mode \"JPN\" comme Tsunoda est un nom japonais mais sans succ\u00e8s j'ai le m\u00eame r\u00e9sultat. Comme la r\u00e9solution est meilleure je me suis dit que peut \u00eatre le filtre de passage en noir et blanc pourrait aider. J'ai \u00e9crit cette petite methode pour convertir l'image en noir et blanc : private static Bitmap ConvertToBlackAndWhite(Bitmap inputBmp) { const int BLACK_TO_WHITE_TRESHOLD = 200; Bitmap result = new Bitmap(inputBmp.Width, inputBmp.Height); for (int y = 0; y < inputBmp.Height; y++) { for (int x = 0; x < inputBmp.Width; x++) { Color pixelColor = inputBmp.GetPixel(x,y); if (pixelColor.R <= BLACK_TO_WHITE_TRESHOLD && pixelColor.G <= BLACK_TO_WHITE_TRESHOLD && pixelColor.B <= BLACK_TO_WHITE_TRESHOLD) { pixelColor = Color.FromArgb(0,0,0); } else { pixelColor = Color.FromArgb(255,255,255); } result.SetPixel(x,y,pixelColor); } } return result; } Rien de bien dingue mais cela fonctionne et je peux jouer avec le BLACK_AND_WHITE_TRESHOLD pour changer son comportement. J'ai dabord test\u00e9 avec un treshold de 100 et le programme a r\u00e9ussi \u00e0 me sortir Tsunoda en deux mots ce qui \u00e9tait d\u00e9ja tr\u00e8s encourageant. Et apr\u00e8s avoir augment\u00e9 le Treshold... Tada : \"Tsunoda 1080P avec filtre\" Le programme arrive bien \u00e0 reconnaitre TSUNODA. Je pense que cette tactique ne fonctionnait pas avant car la resolution \u00e9tait trop faible et l'aliasing se m\u00ealait trop avec le texte pour \u00eatre utilisable. Cependant cette technique ne fonctionne pas sur tous les noms. Par example avec Leclerc : \"Leclerc 1080P avec filtre\" On r\u00e9cup\u00e8re \"Leeler'c\" ce qui n'est pas bon du tout. Mais en modulant le Treshold (ici \u00e0 150) On peut de nouveau voir Leclerc \u00eatre reconnu correctement \"Leclerc 1080P avec filtre 2\" Je pense que pour avoir de bons r\u00e9sultats il va falloir faire un algo qui : D\u00e9coupe l'image en autant de plus petites images pour avoir un mot par image. Teste voir si avec l'image originale un nom correspond \u00e0 la liste de pilotes existant. Si cela ne marche pas, on applique le filtre en modulant le Treshold. Dans le cas ou on aurait pas un match parfait on fait un algo qui cherche le nom le plus proche qui existe dans la liste de noms donn\u00e9s. Seulement voila, il n'y a pas que des lettres que l'on veut r\u00e9cup\u00e8rer. On veut surtout pouvoir r\u00e9cup\u00e8rer les chiffres. Pour les chiffres on va avoir des soucis \u00e9galement... Si on essaie directement la m\u00eame technique sans filtre on a des r\u00e9sultats comme celui ci : \"Tentative de reconnaisance du timing\" La virgule a tendeance \u00e0 se barrer ce qui est particuli\u00e8rement probl\u00e9matique. Cependant comme les chiffres ont beaucoup moins de possibilit\u00e9es que les lettres et qu'il n'y a pas de probl\u00e8me de langue on devrait pouvoir travailler \u00e0 faire des r\u00e8glage que l'on pourra ensuite utiliser. Avec un Treshold de 165 on arrive presque \u00e0 quelque chose d'int\u00e9ressant : \"Tentative 2 de reconnaissance du timing\" Le + n'est clairement pas compris mais ca n'est pas emb\u00eatant car c'est souvent redondant. On arrive cependant \u00e0 isoler 3 et 259. M\u00eame si la virgule n'est pas comprise cela veut dire qu'il est tout de m\u00eame possible de discriminer les secondes des milisecondes. Maintenant avec un temps au tour : \"Reconnaissance du timing au tour\" On arrive sans rien changer aux param\u00eatres \u00e0 isoler minutes secondes et milisecondes. Il semble que la reconnaissance de chiffre soit bien plus efficace que la reconnaissance de lettres. Il va falloir faire un test \u00e0 plus grande \u00e9chelle avec plus d'image pour se rendre compte de la precision. Demain ce qui serait bien cela serait que je fasse un jeu d'images avec des valeurs connues et que je fasse une batterie de tests pour voir \u00e0 quel point je peux faire confiance \u00e0 la reconnaissance des chiffres. Automatiser un syst\u00e8me de test de la sorte me sera tr\u00e8s utile dans le futur pour v\u00e9rifier la non regression de ma reconnaissance de texte quand je tenterai d'y faire des changements. Je suis toujours curieux cependant de voir comment le programme se d\u00e9brouille avec les nombres de tours qui se trouvent dans les icones de pneus. Lundi 3 Avril Aujourd'hui on va faire un programme qui permet de cr\u00e9er un dataset qui permette de tester le programme de reconnaissance. Je pense que le meilleur moyen de faire serait un programme qui cr\u00e9e le dataset et qui ensuite peut tester diff\u00e9rentes methodes de reconnaissance. Par la m\u00eame occasion je peux d\u00e9velopper la technologie qui va permettre de d\u00e9couper une image en 20 lignes ce qui me servira ensuite pour la reconnaissance. Je me rend compte que pour faire un programme de tests je dois d\u00e9ja avoir une id\u00e9e de la structure de mon programme. Pour le moment je r\u00e9flechis \u00e0 un syst\u00e8me de \"Zones\" et de \"Windows\". L'id\u00e9e serait que une Zone est juste une sous partie d'image qui peut encore \u00eatre d\u00e9compos\u00e9 tandis que chaque Window contient une ou plusieurs informations \u00e0 r\u00e9cup\u00e8rer. J'ai essay\u00e9 de d\u00e9couper l'image pour que cela soit plus clair : \"Main zone\" Ici on peut voir que l'image est d\u00e9coup\u00e9e en plusieurs grandes zones. Dans un premier temps on ne s'occupe que de la premi\u00e8re. Ensuite : \"Driver zone #1\" On peut voir la que cette Main zone serait elle m\u00eame d\u00e9compos\u00e9e en plusieurs plus petites zones. Et ensuite chacunes de ces petites zones : \"Driver windows\" Sera d\u00e9compos\u00e9e en plusieurs windows qui elles sont des zones qui contiennent de l'information. En gros on aurait trois types de zone : Les zones qui contiennent d'autres zones Les zones qui contiennent des Windows Les Windows Cependant en y r\u00e9flechissant on pourrait tout \u00e0 fait avoir seulement des zones et des windows en faisant en sorte que les windows peuvent avoir une liste de windows et une liste de zones. Une zone serait compos\u00e9e de : Une image de d\u00e9part Un rectangle qui la positionne sur cette derni\u00e8re Une liste de zones (potentiellement vide) Une liste de windows (potentiellement vide) Une methode qui permet de r\u00e9cup\u00e8rer une image de la zone Une methode qui permet de lancer la reconnaissance sur chaque window Une window serait compos\u00e9e de : Une image de d\u00e9part (cela peut \u00eatre l'image crop\u00e9e de la zone parente peu importe) Un rectangle qui la positionne sur cette derni\u00e8re Une methode qui permet de r\u00e9cup\u00e9rer un image de la window Une methode qui permet de lancer la reconnaisance sur l'image (Chaque type de zone doit l'impl\u00e9menter) Dans chaque window on peut imaginer que la methode qui fait la reconnaissance au lieu de retourner un objet qui peut contenir nimporte quel type d'information peut envoyer ce qu'elle vient de r\u00e9cup\u00e8rer dans une base de donn\u00e9e ou un objet. Par exemple une Zone de pilote pourrait tr\u00e8s bien contenir un objet pilote et le donner \u00e0 ses windows qui rempliraient ce m\u00eame objet. C'est une reflexion plus stockage que OCR mais c'est int\u00e9ressant pour savoir ce que fait une window des donn\u00e9es qu'elle r\u00e9cup\u00e8re. Dans un premier temps je pense que les windows vont simplement \u00e9crire dans un fichier ce qu'elles trouvent chacunes dans le format qu'elles veulent. Pour comprendre pourquoi je me prend la t\u00eate il faut savoir que chaque window peut avoir acc\u00e8s \u00e0 pleins d'informations diff\u00e9rentes. On pourrait dire qu'elles retournent toutes une string sauf que si ca marche pour un temps au tour ou pour un nom de pilote, cela ne marche pas forc\u00e9ment pour un type de pneu ou un DRS ouver. Comme chaque window a plusieurs types de data elle devra elle m\u00eame se charger de comment la traiter ET de la stocker. Voila un diagramme qui r\u00e9sume comment je vois l'impl\u00e9mentation dans un premier temps : \"Diagramme d'explications\" Voici comment se pr\u00e9sente le squellette d'une Zone : public class Zone { private Bitmap FullImage; private List Zones; private List Windows; private Rectangle _bounds; public Rectangle Bounds { get => _bounds; private set => _bounds = value; } public Bitmap ZoneImage { get { Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(FullImage, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); return sample; } } public Zone(Image fullImage, Rectangle bounds) { FullImage = (Bitmap)fullImage; Init(bounds); } public Zone(Bitmap fullImage, Rectangle bounds) { FullImage = fullImage; Init(bounds); } private void Init(Rectangle bounds) { Bounds = bounds; Zones = new List(); Windows = new List(); } public void AddZone(Rectangle bounds) { if(Fits(bounds)) Zones.Add(new Zone(ZoneImage,bounds)); } public void AddWindow(Rectangle bounds) { if (Fits(bounds)) Windows.Add(new Window(ZoneImage,bounds)); } private bool Fits(Rectangle inputRectangle) { if (inputRectangle.X + inputRectangle.Width > Bounds.Width || inputRectangle.Y + inputRectangle.Height > Bounds.Height || inputRectangle.X < 0 || inputRectangle.Y < 0) { return false; } else { return true; } } } Le but est ensuite de cr\u00e9er diff\u00e9rent types de Zones. Par exemple la MainZone devra d\u00e9couper son contenu en 20 parties \u00e9gales pour tenter de chopper les 20 pilotes. Il serait cool de trouver un moyen de calibrer automatiquement. C'est peut-\u00eatre possible de calibrer avec de la reconnaissance de texte, on peut essayer de lancer la reconnaissance et voir ou on trouve du texte avec un peu de chance cela pourrait donner les positions et avec ca on peut peut-\u00eatre determiner des lignes. Et voici le squelette d'une window g\u00e9n\u00e9rique using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace OCR_tester { public class Window { private Bitmap FullImage; private Rectangle _bounds; public Rectangle Bounds { get => _bounds; private set => _bounds = value; } public Bitmap WindowImage { get { Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(FullImage, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); return sample; } } public Window(Bitmap fullImage, Rectangle bounds) { FullImage = fullImage; Bounds = bounds; } public virtual void RecoverInformations() { //Each Window type will have to implement its own way to recover the informations stored in the Window Image } } } Chaque Window pourra ainsi elle m\u00eame impl\u00e9menter la r\u00e9cup\u00e8ration d'informations. La facon de les retourner/stocker est encore un peu floue. Par exemple pour un temps au tour on peut imaginer que il fait une petite v\u00e9rification dans l'objet pilote et dans le tableau des tours si il n'y a pas deja une valeur et si il n'y en a pas une alors il peut l'ajouter. Maintenant je vais essayer de cr\u00e9er une Main window qui se calibre toute seule. Alors apr\u00e8s avoir bien gal\u00e8r\u00e9 avec l'interface pour permettre au user de cliquer sur la form pour voir les zones qu'il cr\u00e9e, j'ai pu cr\u00e9er un zone qui fait les dimensions de MainZone et j'ai pu lancer la reconnaissance sur l'image et voir ou il trouve du texte : \"MainZone avec carr\u00e9s de texte\" Maintenant il faut que je nettoie la liste de rectangle pour exclure ceux qui sont trop grands pour \u00eatre sur une seule ligne, ceux qui indiquent le nombre de tour en haut et ceux qui n'ont pas d'int\u00e9r\u00eats. On pourra ensuite isoler les lignes et cr\u00e9er une liste d'images. Pour ce qui est de la ligne qui contient les \"Gap interval last lap\" et des chiffres sur les tours pour les pneus etc je vais juste demander \u00e0 l'utilisateur de ne pas les prendre dans la screenshot. Comme ils contiennent des mots qui peuvent \u00eatre utilis\u00e9s plus loin dans les data je ne peux pas les blacklister et faire un syst\u00e8me qui s'occupe de les enlever si ils existent selon le position y me prendrait trop de temps pour rien. Apr\u00e8s avoir filtr\u00e9 un peu les resultats et enlev\u00e9 les zones beaucoup trop grandes, on se retrouve d\u00e9ja plus qu'avec ca : \"MainZone avec de meilleurs carr\u00e9s\" Comme on peut le voir, du c\u00f4t\u00e9 gauche de l'image on a beaucoup de choses reconnues mais avec beaucoup de tailles diff\u00e9rentes ce qui n'est pas id\u00e9al. Alors j'ajoute un filtre qui permet de ne selectionner que les data sur la droite. \"MainZone avec de meilleurs carr\u00e9s\" Maintenant il devrait \u00eatre possible de faire un algorythme qui ne prend que un seul carr\u00e9 par ligne. \"MainZone avec un seul carr\u00e9 par ligne\" Maintenant que on sait ou se trouve chaque ligne on peut faire un petit traitement et d\u00e9couper l'image en plusieurs windows. Et voila : \"Mainzone auto calibr\u00e9e\" Maintenant le programme peut cr\u00e9er des zones pour chaque pilote \"Images pilotes\" \"Zone d'un pilote\" Maintenant il faut que j'impl\u00e9mente un syst\u00e8me un peu similaire pour cr\u00e9er des windows. Voici la methode que j'ai cr\u00e9\u00e9 pour l'autocalibration : public void AutoCalibrate() { List detectedText = new List(); Zones = new List(); TesseractEngine engine = new TesseractEngine(Window.tessDataFolder.FullName, \"eng\", EngineMode.Default); Image image = ZoneImage; var tessImage = Pix.LoadFromMemory(Window.ImageToByte(image)); Page page = engine.Process(tessImage); using (var iter = page.GetIterator()) { iter.Begin(); do { Rect boundingBox; if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox)) { //var text = iter.GetText(PageIteratorLevel.Word).ToUpper(); //We remove all the rectangles that are definitely too big if (boundingBox.Height < image.Height / NUMBER_OF_DRIVERS) { //Now we add a filter to only get the boxes in the right because they are much more reliable in size if (boundingBox.X1 > image.Width / 2) { //Now we check if an other square box has been found roughly in the same y axis bool match = false; //The tolerance is roughly half the size that a window will be int tolerance = (image.Height / NUMBER_OF_DRIVERS) / 2; foreach (Rectangle rect in detectedText) { if (rect.Y > boundingBox.Y1 - tolerance && rect.Y < boundingBox.Y1 + tolerance) { //There already is a rectangle in this line match = true; } } //if nothing matched we can add it if(!match) detectedText.Add(new Rectangle(boundingBox.X1, boundingBox.Y1, boundingBox.Width, boundingBox.Height)); } } } } while (iter.Next(PageIteratorLevel.Word)); } foreach (Rectangle Rectangle in detectedText) { Rectangle windowRectangle; Size windowSize = new Size(image.Width, image.Height / NUMBER_OF_DRIVERS); Point windowLocation = new Point(0, (Rectangle.Y + Rectangle.Height / 2) - windowSize.Height / 2); windowRectangle = new Rectangle(windowLocation, windowSize); Zones.Add(new Zone(ZoneImage, windowRectangle)); } } Ca peut paraitre pas \u00e9norme comme code mais pour tout mettre en place ca demande quand m\u00eame pas mal de reflexion. J'ai du clean un peu le code que j'avais fait pour permettre la selection de zones et ajouter la possibilit\u00e9 d'ajouter des windows sur une zone. J'ai juste quelques difficult\u00e9es \u00e0 les ajouter correctement, j'ai un offset tout pourri qui se met tout le temps \"Sainz coup\u00e9\" \"Perez coup\u00e9\" Cela doit \u00eatre un soucis lors de la detection de clic qui met un offset en trop. C'est vraiment p\u00e9nible en tout cas. Certes c'est moins fun de devoir manuellement indiquer ou sont les windows sur une ligne de pilote, mais je ne vois vraiment pas comment faire cela automatiquement. Le but c'est de faire une configuration qui puisse \u00eatre sauvegard\u00e9e comme ca pas besoin d'\u00e0 chaques fois le refaire. C'est bon ! J'avais juste oubli\u00e9 de changer le calcul d'offset entre le code de la zone et de la window. Note pour plus tard, il serait peut-\u00eatre judicieux de faire quelque chose pour la vue, les windows et les Zones ont le m\u00eame exact comportement pour la vue ce qui fait dupliquer du code. Mais au moins maintenant ca fonctionne : \"Ocr tester screenshot\" Et le programme va directement cr\u00e9er un dossier par pilote avec toutes les images de chaque Data le concernant : \"Dossier Perez\" ; Et c'est tout pour aujourd'hui je pense. Ce qui serait cool demain c'est que je puisse stocker d'une mani\u00e8re ou d'une autre ces fichiers de calibration et que je puisse les transf\u00e8rer vers le programme qui va s'occuper de d\u00e9coder et commencer gentillement \u00e0 d\u00e9coder les diff\u00e9rents types de data. Note pour quand je ferai les tests. Je pense que la meilleure id\u00e9e serait que je prenne pleins de photos du style et que je les mette dans un fichier CSV ou JSON avec leur contenu. Et ensuite je le fais passer en tests pour calculer la prescision de mon algo de d\u00e9codage. Pour le moment on est plut\u00f4t dans les clouts niveau planning. Mardi 4 Avril Aujourd'hui je suis scens\u00e9 plut\u00f4t bosser sur l'interpretation des donn\u00e9es, mais une id\u00e9e m'a taraud\u00e9 l'esprit toute la nuit. Est-ce que je ne pourrais pas quand m\u00eame essayer de d\u00e9composer la zone de pilote directment comme pour la Main zone. Pour ce faire j'ai tent\u00e9 de faire comme pour la main zone c'est \u00e0 dire lancer la reconnaissance pour savoir ou \u00e9taient tous les champs de donn\u00e9es mais malheureusement je ne pense pas que cela va \u00eatre possible. En effet non seulement ici les champs sont de tailles tr\u00e8s vari\u00e9es, mais en plus la reconnaissance n'arrive pas \u00e0 en r\u00e9cup\u00e8rer le m\u00eame nombre sur chaque ligne ce qui risque d'\u00eatre complexe \u00e0 utiliser ensuite. La preuve : \"Tentative d'auto calibration\" ; Cependant tout n'est pas perdu ! Il y a peut-\u00eatre un moyen qui serait mieux en tous points. Le soucis avec ce type de reconnaissance c'est qu'on utilise beaucoup de ressources inutiles. On peut peut-\u00eatre hard coder la valeur des diviseurs et les utiliser pour cr\u00e9er des zones. Ok alors visiblement c'est un probl\u00e8me car il semble y avoir d'autres pixels de cette couleur dans l'image (Qui l'aurait cru lol) \"Tentative 2\" J'a tent\u00e9 de r\u00e9duire la tol\u00e9rance mais le soucis c'est que c'est soit trop soit pas assez Derni\u00e8re tentative, j'ai essay\u00e9 de prendre plusieurs pixels en hauteur pour chaque incr\u00e9ment de X et en faire la moyenne, et m\u00eame comme ca, impossible de trouver de mani\u00e8re efficace les zones. Je pense que je vais donc revert tous mes changements pour revenir \u00e0 la version ou on les choisissait manuellement. Pas mal de temps perdu mais bon c'est comme ca ca arrive Bon j'ai fait un revert mais j'ai ajout\u00e9 une feature importante. Les zones font la largeur indiqu\u00e9e par l'utilisateur mais elles font la hauteur max comme ca toutes les window font la m\u00eame hauteur et ca permet \u00e0 l'utilisateur de ne pas forc\u00e9ment \u00eatre ultra pr\u00e9cis dans sa selection. Ce qui nous donne : \"Resultat final\" Maintenant je dirais que les deux prochaines choses \u00e0 faire seraient de stocker ces zones dans un fichier JSON ou autre pour que la calibration puisse \u00eatre envoy\u00e9e directement dans le logiciel de reconnaissance et ensuite de faire une calibration sur des images qui font la taille qu'on aura pendant les Grands Prix. Pour le moment elles sont au format 16:10 qui est le format d'\u00e9crant de mon laptop. Pour le stockage j'imagine un fichier qui donne des indications assez simples qui permettent de reconstruire le total des zones quand il est import\u00e9 plutot que d'\u00e9crire les coordonn\u00e9es en dur pour chacunes. Chaque Grande zone va impl\u00e9menter une methode qui s'occupe de mettre tous ses enfants dans un fichier. { \"MainZone\":{ \"x\": 10, \"y\": 20, \"width\": 1450, \"height\": 1340, \"DriverZone\":{ \"x\": 0, \"y\": 23, \"height\": 25, \"Windows\":[ { \"DriverPositionWindow\":{ \"x\": 0, \"y\": 0, \"width\": 35 } }, { \"DriverPositionChangesWindow\":{ \"x\": 0, \"y\": 0, \"width\":45 } } ] } } } C'est le r\u00e9sultat auquel j'aimerais arriver. Mais pour y arriver il faut encore que je cr\u00e9e les diff\u00e9rents types de window. Cela veut dire que je dois d\u00e9cider quelles informations je vais r\u00e9cup\u00e8rer de la page. Par exemple je vais conserver la position du pilote mais au final les changements de positions sont difficiles \u00e0 lire et sont redondants. Si je garde un historique des positions des pilotes je peux calculer moi m\u00eame les changements. Pareil pour gap avec la voiture devant. Je pense que je vais juste garder l'information des \u00e9carts absolus et ensuite je pourrai toujours calculer la diff\u00e9rence entre les pilotes. Ca peut para\u00eetre b\u00eate car cela rajoute du calcul mais en r\u00e9alit\u00e9 le calcul de l'OCR est extr\u00eamement gourmand alors il faut que j'\u00e9vite le plus possible d'y faire recours. Il est bien plus rapide de calculer les \u00e9carts que d'essayer de reconnaitre le texte et le convertir en chiffre. J'ai visiblement ajout\u00e9 un bug dans mon code. Maintenant tous les pilotes ont la m\u00eame image quand on les selectionne. Mais visiblement ca n'\u00e9tait pas le cas avant car j'avais pu prendres des images de chaque pilote. J'ai pass\u00e9 3 minutes \u00e0 fixer un bug stupide j'ai un peu envie de br\u00fbler ma place de travail... Mais bon au moins maintenant cela fonctionne ! Toutes les images sont r\u00e9cup\u00e8r\u00e9es et ont un format correct avec le bon nom : \"Verstappen folder\" Avec un peu de code tr\u00e8s moche j'ai pu cr\u00e9er un fichier JSON qui contient les diff\u00e9rentes infos. Cependant en exportant TOUT on se retrouve avec un fichier de 1200 lignes ce qui n'est pas optimal. Mais quand on regarde, il devrait \u00eatre possible de faire un fichier qui ne contient que les infos d'un seul pilote car ensuite il y a simplement un offset \u00e0 appliquer sur la zone et les windows. Je vais donc pouvoir commencer enfin le logiciel de d\u00e9codage qui prend en entr\u00e9e un fichier JSON comme celui ci qui a \u00e9t\u00e9 g\u00e9n\u00e8r\u00e9 avec le programme de calibration. { \"Main\": { \"x\": 40, \"y\": 230, \"width\": 1845, \"height\": 719, \"Zones\": [ { \"DriverZone\": { \"x\": 0, \"y\": 3, \"width\": 1845, \"height\": 35, \"Windows\": [ { \"Position\": { \"x\": 2, \"y\": 0, \"width\": 32 }, \"GapToLeader\": { \"x\": 204, \"y\": 0, \"width\": 96 }, \"LapTime\": { \"x\": 413, \"y\": 0, \"width\": 105 }, \"Drs\": { \"x\": 526, \"y\": 0, \"width\": 81 }, etc... } ] } } ] } } Dans le futur il faudrait ajouter d'autres choses comme par exemple les diff\u00e9rents pilotes pr\u00e9sents sur le Grand Prix et ce genre d'infos. Quoique je vais l'ajouter d\u00e9ja maintenant et plus tard je mettrai en place la feature acessible depuis l'interface. Mais le hardcoder maintenant me permet d\u00e9ja de mieux coder l'autre c\u00f4t\u00e9. Ce programme n'est en aucun cas termin\u00e9 et je vais devoir travailler encore un peu dessus pour qu'il soit utilisable correctement mais au moins il fonctionne \u00e0 peu pr\u00e8s. Exemple du json avec les noms de pilotes: { \"Main\": { \"x\": 37, \"y\": 238, \"width\": 1851, \"height\": 713, \"Zones\": [ { \"DriverZone\": { \"x\": 0, \"y\": -5, \"width\": 1851, \"height\": 35 } } ] }, \"Drivers\": [ \"Leclerc\", \"Verstappen\", etc... ] } Maintenant je vais m'attaquer au d\u00e9codage. Demain je dois finir le d\u00e9codage du JSON et je dois commencer \u00e0 impl\u00e9menter la reconnaissance des textes. Voire m\u00eame des pneus etc si j'y arrive. Mercredi 5 Avril Bon la il faut vraiment que je finisse assez vite la lecture du JSON et la reconstruction des zones pour commencer la reconnaissance. J'ai pris beaucoup de temps \u00e0 faire le programme de calibration mais je pense que c'est essentiel de prendre ce temps maintenant. (BTW il faudra quand m\u00eame retourner faire une plus jolie version par ce que la ca marche mais c'est tout) Bon apr\u00e8s pas mal de boulot je pense avoir r\u00e9ussi. Dans le nouveau programme on arrive \u00e0 r\u00e9cup\u00e8rer les diff\u00e9rentes zones : \"JSON decode result\" Un conseil de notre professeur M.Bonvin a \u00e9t\u00e9 de cr\u00e9er des Releases de versions qui ne fonctionnent pas ou pas tr\u00e8s bien. J'ai donc publi\u00e9 une premi\u00e8re release de l'OCR_TEST qui fonctionne vite fait. J'ai seulement un petit soucis, comme je recr\u00e9e compl\u00eatement la structure des driver zones avec seulement la premi\u00e8re, il y a un petit d\u00e9calage car entre les zones il y avait un gap. Ce qui fait que si la premi\u00e8re zone est parfaitement centr\u00e9e : \"Centered driver\" La vingti\u00e8me ne l'est plus exactement : \"Uncentered Driver\" Pour ca j'ai essay\u00e9 de mettre un espacement arbitraire mais c'est complexe. Je vais plut\u00f4t tenter de faire une diff\u00e9rence entre la taille de la zone compl\u00eate et de la taille additionn\u00e9e de toutes les fen\u00eatre et diviser le resultat entre toutes les fen\u00eatres. Ca n'est pas parfait mais au moins maintenant les donn\u00e9es ne touchent plus les bords de la fen\u00eatre. Et voila ! Maintenant avec le fichier de configuration en Json on arrive \u00e0 r\u00e9cup\u00e8rer toutes les infos comme si elles avaient \u00e9t\u00e9 envoy\u00e9es directement depuis l'app de calibration mais sans le processing time ! \"Verstappen folder 2 On peut donc ENFIN passer au d\u00e9codage de ces FICHUES donn\u00e9es. Je vais pouvoir impl\u00e9menter ce que j'ai fait dans le projet de test de d\u00e9codage. Gr\u00e2ce \u00e0 mon d\u00e9coupage initial qui m'a pris du temps \u00e0 impl\u00e9menter on a enfin un truc qui marche m\u00eame si je n'ai impl\u00e9ment\u00e9 que la reconnaissance de noms. \"Image reconnaissance propre\" Si on se rappelle du syst\u00e8me de window et de zones dans le diagramme plus haut, c'est assez facile de comprendre comment je m'y suis pris. En gros on des listes et des listes de listes de zones, c'est la partie un peu plus technique car il y a des zones qui peuvent contenir d'autres zones etc. Je vais commencer par la reconnaissance de noms. Voici le tableau de pilotes de 2023 \"Drivers\": [ \"Leclerc\", \"Verstappen\", \"Hamilton\", \"Alonso\", \"Russel\", \"Gasly\", \"Stroll\", \"Sainz\", \"Hulkenberg\", \"Norris\", \"Tsunoda\", \"Piastri\", \"Zhou\", \"Ocon\", \"Magnussen\", \"Perez\", \"Sargeant\", \"De Vries\", \"Bottas\", \"Albon\" ] ET voici le tableau de pilotes de 2022 : \"Drivers\": [ \"Leclerc\", \"Verstappen\", \"Sainz\", \"Perez\", \"Hamilton\", \"Russel\", \"Magnussen\", \"Gasly\", \"Ocon\", \"Alonso\", \"Tsunoda\", \"Bottas\", \"Zhou\", \"Albon\", \"Stroll\", \"Schumacher\", \"Hulkenberg\", \"Norris\", \"Latifi\", \"Ricciardo\" ] Je les notes ici car J'ai souvent besoin de changer selon le dataset que j'utilise. Dans le futur je ferai s\u00fbrement un grand dataset qui prend des pilotes de reserves et des pilotes juniors pour que dans le cas ou un pilote est remplac\u00e9 dans l'ann\u00e9e il n'y a pas besoin de tout recalibrer avec l'application. Apr\u00e8s une discussion avec M.Bonvin j'ai d\u00e9cid\u00e9 de tester 3 valeurs de convertion en noir et blanc et si je ne trouve pas un match exact je prend le nom le plus proche. Pour trouver la string la plus proche je pense que je vais utiliser quelque chose qui s'appelle la technique de Levenshtein. De ce que j'ai compris c'est un algorythme qui permet de donner une metric de diff\u00e9rence entre deux strings. Bon et \u00e9videmment il ne faut pas se tromper dans la liste des pilotes GENRE NE PAS OUBLIER QUE GEORGE RUSSELL COMPORTE DEUX WFNEWIEWV DE \"L\" A LA FIN DE SON NOM CE QUI POURRAIT ENGRANGER 2H DE DEBUGGING POUR RIEN ASK ME HOW I KNOW joker laugh J'ai vraiment un soucis avec Tsunoda, Il a trop tendeance \u00e0 le confondre avec \"TSUNDDA\" et pour des raisons obscures, quand j'applique l'algorythme de Levenshtein le plus proche n'est pas \"Tsunoda\" mais \"Sainz\" iniuvbwdiucbiubisc POURQUOI !!??!! Je pense que cela demande moins de changements de lettres enfin bon c'est quand m\u00eame pas id\u00e9al. Il va falloir que je trouve un moyen de le repond\u00e9rer. C'est dommage par ce que cela marche super avec Alonso Verstappen et Albon. J'ai un peu modifi\u00e9 la methode et j'ai fait en sorte d'envoyer tous les noms en majuscules en me disant que cela pourrait r\u00e9duire le nombre de changements. Et ca a march\u00e9 !! Cela va s\u00fbrement demander plus de tests pour \u00eatre bien s\u00fbr que tout fonctionne nikel, cependant pour le moment ca marche parfaitement avec les pilotes de 2022. Pour ce qui est de la reconnaissance de chiffres, j'ai d\u00e9ja fait une partie du boulot le premier jour alors je vais juste reprendre \u00e0 partir de l\u00e0. Je r\u00e9cup\u00e8re une string de ce type \"1:35.123\" le soucis c'est que les : se transforment parfois en . ou inversement mais bon ca devrait pas \u00eatre trop dur \u00e0 g\u00e8rer. Il faut que je transforme cette string en nombre de milisecondes (Du moins je pense que c'est le meilleur moyen pour ensuite pouvoir facilement comparer et stocker l'information). Cela fait que 1:35:123 en milisecondes donne : 1 * 1000 * 60 => 60'000 35 * 1000 => 35'000 123 => 123 Total : 60'000+35'000+123 => 95'123ms Et pour l'affichage : Minutes = ms / 60'000 secondes = (ms - (minutes/60'000))/1000 ms = ms - ((minutes 60'000) + (secondes * 1000)) Et on se retrouve avec 1:35:123 Maintenant apr\u00e8s un peu de temps pour nettoyer la string etc on se retrouve avec un r\u00e9sultat comme le suivant : Position : 0 Gap to leader : 0:0:0 Lap time : 2:15:123 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : LECLERC Sector 1 : 0:31:323 Sector 2 : 0:42:340 Sector 3 : 0:0:0 Evidemment pareil pour les autres pilotes Et je me rend compte que j'ai encore tout cass\u00e9 car le laptime ne devrait pas \u00eatre 2:15 mais 1:35... Voila apr\u00e8s une heure de debugging et des ajouts pour nettoyer les chaines on se retrouve avec : Position : 0 Gap to leader : 0:0:0 Lap time : 1:35:123 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : LECLERC Sector 1 : 0:31:323 Sector 2 : 0:42:340 Sector 3 : 0:0:0 Note: le traitement commence \u00e0 devenir long, il serait peut-\u00eatre int\u00e9ressant d'utiliser un seul Tesseract Engine ou de voir ce qui prend autant de temps, on d\u00e9passe la seconde de traitement ce qui est un peu ma limite. Apr\u00e8s on peut toujours tester de rajouter du multicore processing mais c'est pour une autre fois. Demain je m'occupe de r\u00e8gler les soucis que j'ai avec la prescision de ces temps au tour et j'\u00e9sp\u00e8re pouvoir m'occuper aussi de la position des pneus et du DRS. J'aimerais finir tout ca cette semaine. Jeudi 6 Avril Une id\u00e9e m'est pass\u00e9e par la t\u00eate pendant que je dormais, dans la liste des pilotes, quand ils sont \u00e0 plus d'un tour de retard avec le leader (Ce qui arrive normalement dans presque tous les Grand Prix) on a pas des minutes mais une string qui montre \"+1 Lap\" ou \"+2Laps\" ce qui est \u00e9videmment un probl\u00e8me. Je pense qu'une bonne facon d'envoyer l'info serait de retourner -1 -2etc... \u00e0 la place des milisecondes, mais encore faut-il detecter le nombre de tours Je devrais \u00eatre en train de commencer la documentation de commment tout ce que j'ai fait fonctionne. Cependant je ne me vois pas faire ca tant que je n'ai pas au moins r\u00e9cup\u00e8r\u00e9 toutes les infos au moins un peu proprement. Cela veut dire que je commence officiellement \u00e0 prendre du retard. (Sachant que si je finis tout aujourd'hui une journ\u00e9e de doc suffira largement le terme est un peu exag\u00e8r\u00e9 mais bon) Bon pour la reconnaissance des temps c'est sp\u00e9cial... Le filtre semble ne pas changer grand chose ce qui est probl\u00e9matique et ca n'est vraiment pas fiable. Voici quelques expemples avec un treshold de 100: \"11ZSD\" Cette image est comprise comme \"11ZSD\" 42340 Cette image est comprise comme \"42340\" \"ZZAEB\" Et celle ci \"ZZAEB\"... Ce qui... n'est pas bon du tout... J'ai essay\u00e9 de trouver un fichier d'entrainement sp\u00e9cifiquement fait pour les digits. J'ai essay\u00e9 de blacklister les chars non voulus pour tenter d'obliger Tesseract \u00e0 trouver des chiffres. Avec la premi\u00e8re option, les r\u00e9sultats ne sont pas meilleurs voire pires. Avec la seconde option c'est d\u00e9ja pas mal mieux mais on perd compl\u00e8tement la possibilit\u00e9 de detecter les mots comme \"LEADER\" ou \"LAP\" et de toute facon ca n'est pas parfait. Le soucis c'est que si je n'ai pas des donn\u00e9es fiables c'est juste impossible de faire des calculs et de l'affichage correct... Il faut absolument que je trouve une solution. J'ai essay\u00e9 d'utiliser de l'interpolation our augmenter la taille de l'image et ensuite appliquer mon filtre pour retirer le flou mais sans succ\u00e8s... Pourtant la on se retrouve avec des images plut\u00f4t claires : \"Clear1\" Ici le programme trouve \"44301\" \"Clear2\" Et ici \"A5151\"... On a toujours les m\u00eames probl\u00e8mes. Bon je suis all\u00e9 me renseigner sur l'OCR et je me suis dit que j'allais tenter de faire les choses proprement. Je vais faire passer plusieurs \u00e9tapes de postProcessing avant de donner l'image \u00e0 Tesseract. GrayScale Tresholding InvertColors Scaling Dilatation Ce qui donne : \"Original\" \"Grayscale\" \"InvertColors\" ; \"Resize\" ; \"Dilatation\" Ce qui ne change : Roulement de tambour RIEN kjd viuwvuirnvoirenbf Tout ca pour rien... C'EST BON !!! Bon en fait au final le probl\u00e8me \u00e9tait une mauvaise configuration de Tesseract. Je vais devoir un peu nettoyer tout ca. Mais avec les changements de l'image on a des r\u00e9sultats BEAUCOUP plus pr\u00e9cis et potentiellement utilisables. La je vais devoir faire un serieux travail de nettoyage et simplification de mon code par ce que la c'est vraiment un chantier vu le nombre de choses que j'ai du essayer. J'ai du aussi beaucoup modifier la gestion de l'image ce qui donne : \"Clean\" Et la on a des r\u00e9sultats qui sont vraiment bons. J'ai pu ajouter assez facilement la detection de position comme c'est simplement un chiffre. On se retrouve maintenant avec ce genre de retours : Position : 1 Gap to leader : 8:33:51 Lap time : 2:19:123 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : LECLERC Sector 1 : 0:31:828 Sector 2 : 0:42:940 Sector 3 : 0:0:0 Position : 2 Gap to leader : 0:3:259 Lap time : 23:12:392 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : VERSTAPPEN Sector 1 : 0:38:119 Sector 2 : 0:0:0 Sector 3 : 0:0:0 Il ne manque plus que l'impl\u00e9mentation de la reconnaissance du DRS et des Pneus Et non... je viens de me rendre compte que mon programme a encore cass\u00e9 car le tap time ne peut pas \u00eatre 23 min lol. J'ai un nouveau magnifique probl\u00e8me... Les points et les deux points sont interpr\u00eat\u00e9s comme des chiffres ... Give me a F * * break... J'ai du mal \u00e0 comprendre pourquoi ils ne sont d\u00e9tect\u00e9s comme tels que maintenant. Bon alors il semblerait les temps au tour aie besoin d'un ordre tr\u00e8s pr\u00e9cis pour fonctionner. Grayscale InvertColors Tresholding Resize * 2 Resize * 2 Et la on a des r\u00e9sultats un peu mieux. Bon demain il faut absolument que je me charge de r\u00e8gler tous ces probl\u00e8mes et que je commence la reconnaissance des pneus et de DRS par ce que je commence \u00e0 \u00eatre en retard. Vendredi 6 Avril 2023 Alors aujourd'hui c'est le dernier jour avant de commencer \u00e0 \u00eatre en retard pour de bon. J'ai r\u00e9ussi \u00e0 r\u00e8gler le probl\u00e8me des temps au tour, des gaps, et des secteurs. Dans le processus j'ai cass\u00e9 la detection de position mais ca devrait pas \u00eatre TROP compliqu\u00e9. Et voila ... Apr\u00e8s seulement plus de dix heures de gal\u00e8re, si on donne cette image au programme et le bon JSON le programme nous retourne : Position : 1 Gap to leader : 0:05:059 Lap time : 1:39:123 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : LECLERC Sector 1 : 0:31:828 Sector 2 : 0:42:940 Sector 3 : 0:00:000 Position : 2 Gap to leader : 0:03:259 Lap time : 1:39:392 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : VERSTAPPEN Sector 1 : 0:31:749 Sector 2 : 0:00:000 Sector 3 : 0:00:000 Evidemment le GapToLeader est faux sur leclerc car il est leader mais bon ca je pourrai toujours Hardcoder que le premier a jamais de GapToLeader. Bon j'ai eu beaucoup de soucis que je ne vais pas mentionner ici car ce sont simplement des soucis de logique de programmation pour trouver un DRS ouvert ou non. Au final la technique que j'utilise et qui marche plut\u00f4t bien pour le DRS est que je prend la premi\u00e8re image de DRS et je la d\u00e9clare comme valeur \u00e9talon d'un DRS non actif, en effet dans 99% des cas le leader n'a pas de DRS (cela peut arriver alors il faudra donc juste verifier que les pilotes sont bien \u00e0 moins de deux secondes les uns des autres pour confirmer). Ensuite cette valeur \u00e9talon je la calcule en fonction du nombre de pixels verts dans l'image et si il y a plus de 30% de pixels verts en plus c'est que le DRS est activ\u00e9 ex: Ceci est un DRS ferm\u00e9: \"Closed DRS\" Ceci est un DRS ouvert: \"Open DRS\" Cela marche \u00e0 peu pr\u00e8s tout le temps mais dans le pire des cas on peut toujours verifier que les pilotes sont bien proches pour detecter les potentiels rares cas de faux positifs. J'ai pu augmenter les performances en utilisant un seul engine pour tout le monde et en arr\u00eatant d'utiliser GetPixel et SetPixel qui sont simplement des horreurs \u00e0 utiliser. Mais elles ne sont pas encore bonnes Le soucis avec la detection de pneus cependant, c'est qu'il n'est pas possible d'utiliser la reconnaissance pour savoir ou regarder la couleur car cela ne marcherait pas. Je ne peux pas faire trop de post processing car je dois conserver la couleur Je ne peux pas hardocder un endroit ou aller regarder car cela \u00e9volue tout le long du Grand Prix. Bref c'est la gal\u00e8re. En y r\u00e9flechissant je me suis dit qu'une bonne id\u00e9e pourrait \u00eatre de partir de la droite de la zone du pneu en regardant au milieu de la hauteur. Puis continuer vers la gauche jusqu'\u00e0 ce que je rencontre une couleur diff\u00e9rente. Je pourrai ensuite faire une zone un peu vers la gauche qui devrait contenir les infos du pneu et sur laquelle il sera possible de faire de le reconnaissance de couleur et de la reconnaissance de chiffres. J'ai d\u00e9termin\u00e9 que le background n'\u00e9tait jamais plus clair que #505050 et que donc nimporte quelle couleur qui aurait plus que 50 dans un seul des channels serait consid\u00e8r\u00e9e comme une couleur cassant le background Pour arriver \u00e0 cette conclusion je me suis amus\u00e9 un peu avec les couleurs pour jouer avec les limites de mon algorythme : \"Color fun\" Et je crois que j'ai eu une bonne id\u00e9e, avec une petite methode bien faite on arrive \u00e0 de supers r\u00e9sultats : private Rectangle FindTyreZone() { Bitmap bmp = WindowImage; int currentPosition = bmp.Width; int height = bmp.Height / 2; Color limitColor = Color.FromArgb(0x50,0x50,0x50); Color currentColor = Color.FromArgb(0,0,0); Size newWindowSize = new Size(bmp.Height,bmp.Height); while(currentColor.R <= limitColor.R && currentColor.G <= limitColor.G && currentColor.B <= limitColor.B && currentPosition > 0) { currentPosition--; currentColor = bmp.GetPixel(currentPosition,height); } //Its here to let the new window include a little bit of the right side int offset = Convert.ToInt32((float)newWindowSize.Width / 100f * 20f); int CorrectedX = currentPosition - (newWindowSize.Width - offset); if (CorrectedX <= 0) return new Rectangle(0,0,newWindowSize.Width,newWindowSize.Height); return new Rectangle(CorrectedX,0,newWindowSize.Width,newWindowSize.Height); } \"Tyres\" Maintenant cela devrait \u00eatre beaucoup plus simple de trouver la couleur g\u00e9n\u00e9rale et le nombre de tours. Donc ce que je fais c'est que je fais une reconnaissance de texte sur l'image r\u00e9duite. Si je trouve une lettre c'est facile Ca me donne le type de pneu et ca me dit que c'est le premier tour avec. Si c'est un nombre alors je fais la moyenne de toutes les couleurs de l'image et je prend la couleur de pneu la plus proche. Voici les diff\u00e9rentes couleurs de pneus : SOFT : #FF0000 MEDIUM : #f5bf00 HARD : #d9d8d4 INTER : #00a42e WET : #2760a6 \"Tyre colors\" Les couleurs de pneus peuvent changer de temps \u00e0 autres, par exemple cette r\u00e8gle de pneus est arriv\u00e9e en 2019 et avant il y avait beaucoup plus de couleurs mais dans une volont\u00e9 de rendre le sport plus facile \u00e0 comprendre \u00e0 la t\u00e9l\u00e9 cela a \u00e9t\u00e9 simplifi\u00e9. Je ne pense pas que cela va changer dans les ann\u00e9es qui viennent alors tout est hardcod\u00e9. Je pense que j'ai des soucis avec la detection de texte et de couleur car ma zone est trop grande. Alors bon j'\u00e9crit ces lignes apres des heures de tests. Il semble que la principale difficult\u00e9 avec ces pneus c'est que les chiffres ou lettres sont minuscules. Il est donc extr\u00eamement difficile de faire une reconnaissance ne serait-ce qu'un peu fiable.. Je fais de mon mieux pour tenter de r\u00e8gler le soucis cependant c'est vraiment complexe. Je commence \u00e0 devenir fou, je tente tout et nimporte quoi pour permettre \u00e0 mon algo de fonctionner et m\u00eame quand je fais du post processing comme pas possible il me retourne toujours nimporte quoi... \"5i t'inqui\u00e8tes\" Ici le programme va trouver '5i'... En fait c'est complexe d'expliquer tout ce que je fais car je change tout en boucle en essayant et en ratant ce qui prend des heures. Pour aujourd'hui j'abandonne je vais simplement rentrer chez moi et y r\u00e9flechir cette nuit mais je ne vois pas comment mieux faire la... C'est terrible par ce que je sens que je ne suis pas bien loin. Vacances Bon je vais un peu laisser de c\u00f4t\u00e9 la detection de chiffres pour me pencher un peu plus sur la d\u00e9tection de couleur. Par ce que techniquement si j'arrive \u00e0 toujours parfaitement la detecter alors je pourrais me passer des chiffres car ils sont redondant si je construit un historique de pneus. J'ai r\u00e9ussi \u00e0 fix mon probl\u00e8me de mauvaise detection de couleur de pneus. Du moins je crois. Seulement j'ai quand m\u00eame un souci, les fen\u00eatres de pneus avec une lettre n'ont pas assez de couleur pour \u00eatre d\u00e9tect\u00e9s. Je vais donc essayer de detecter les cinq lettres possibles et si il ne trouve pas alors je pourrai tenter de detecter les chiffres sans lettres ce qui devrait grandement aider. Le but est encore une fois de r\u00e9duire les possibilit\u00e9s de Tesseract. Je me rend de plus en plus compte que le plus important c'est de r\u00e9duire le scope le plus possible. Moins il y a de mots et lettres et de chiffres possibles meilleure sera la reconnaissance. Bon ca ne veut toujours pas marcher maintenant le 11 est interpr\u00eat\u00e9 comme trois I ou comme un M... J'en ai marre sans rire c'est vraiment p\u00e9nible. Alors j'\u00e9crit ces lignes deux jours plus tard et me rend compte avec horreur que toutes mes modifications sur ce journal de bord n'ont pas \u00e9t\u00e9 auvegard\u00e9e... yess.. Bon pour faire simple, j'ai r\u00e9ussi \u00e0 rendre la detection de couleurs bien plus efficace en r\u00e9duisant la taille de l'image et en ne prenant pas en compte les couleurs que l'on d\u00e9tecte comme \u00e9tant partie int\u00e9grante du background. Par exemple quand on a une image comme celle ci : \"Avec background\" qui contient un background alors que ci dessous, on l'a enlev\u00e9. \"Sans background\" La diff\u00e9rence est t\u00e9nue mais elle permet de grandement am\u00e9liorer la prescision de la reconnaissance de couleurs. Pour ce qui est du nombre de tours je me suis rendu compte que cela n'\u00e9tait d\u00e9ja pas tr\u00e8s utile car avec l'historique on devrait pouvoir le d\u00e9duire. Mais bon pour la forme je me suis dit que cela serait quand m\u00eame une bonne id\u00e9e de v\u00e9rifier avec la reconnaissance. J'\u00e9tais quasi certain que le soucis \u00e9tait le fait que l'on voie le contour du logo de pneu qui faisait que la reconnaissance avait du mal. Et j'avais raison ! En les enlevant (Ce qui n'a pas \u00e9t\u00e9 simple) J'ai pu avoir des chiffres beaucoup plus proches de la r\u00e9alit\u00e9. En m\u00eame temps je ne vois pas bien comment j'aurais pu faire mieux : \"Super 11\" Je suis quand m\u00eame assez fier de voir que j'ai r\u00e9ussi \u00e0 part de l'image que on peut voir un peu plus haut et automatiquement la transormer en celle ci-dessus. J'ai donc pu retirer le round autour du chiffre et cela m'a permit de pouvoir d\u00e9zoomer un peu et c'est avec ca que les lettres ont pu \u00eatre mieux reconnues : \"Super H\" \"Super M\" Maintenant je pense qu'il ne reste \"plus qu'\u00e0\" nettoyer un peu tout ce code qui traine de partout pour tout faire fonctionner et impl\u00e9menter un peu de parrallel processing ainsi que de l'asynchrone pour ne pas bloquer le reste du programme. Par ce qu'il faut savoir que en l'\u00e9t\u00e2t, le programme met 25 secondes \u00e0 d\u00e9marrer et consomme presque 2GB de Ram. Certes cela ne veut pas dire que la reconnaissance \u00e0 elle seule prend 25 secondes car au d\u00e9marrage il y a aussi la lecture du fichier de config et la cr\u00e9ation des window etc.. En r\u00e9alit\u00e9 la partie strictement OCR prend dans les 12s si on en croit la fonction stopWatch de C#. Et quand on change d'image la reconnaissance prend 9s. Dans tout les cas c'est BEAUCOUP trop. J'aurais eu comme objectif de faire une reconnaissance toutes les secondes. Je ne sais pas bien si cela va \u00eatre possible mais en tout cas le but va \u00eatre de s'en rapprocher. Pour \u00eatre plus exact et permettre une comparaison, voici les stats exactes Avec un fichier d'images vide : Loading - 11.8s Splitting d'images - 90ms OCR - 12.5s Avec un fichier d'images plein : Loading - 10.8s Splitting d'images - 80ms Ocr - 11.6s En passant d'une image \u00e0 l'autre : Loading - NaN Splitting d'images - 50ms Ocr - 8.8s Donc on peut voir que les deux endroits ou le programme prend le plus de temps c'est au premier d\u00e9marrage quand il faut lire le fichier et setup les windows etc... Et l'OCR qui prend un temps fou. Ce qui est pratique c'est que les presque 2gb de ram sont utilis\u00e9s que au lancement et ensuite l'application n'en utilise que quelques centaines de mb. Le processeur lui tourne entre 10 et 20% ce qui ne va pas durer :) Je vais m'occuper dabord du loading. J'ai essay\u00e9 d'utiliser un Parrallel.For au moment de la cr\u00e9ation des windows, le probl\u00e8me c'est que visiblement les objets windows sont beaucoup trops complexes et utilisent trop de ressources partag\u00e9es pour \u00eatre vraiment thread Safe. J'\u00e9sp\u00e8re que je n'aurais pas trop de soucis avec ca qu'en j'en viendrai \u00e0 l'optimisation de l'OCR... Ce qui me rend fou c'est que cette boucle toute nulle prend plus de dix secondes \u00e0 s'executer et je ne comprend pas bien pourquoi. for (int i = 0; i < NUMBER_OF_DRIVERS; i++) { Point tmpPos = new Point(0, FirstZonePosition.Y + i * FirstZoneSize.Height - Convert.ToInt32(i * offset) /*- (i* (FirstZoneSize.Height / 32))*/); Zone newDriverZone = new Zone(MainZoneImage, new Rectangle(tmpPos, FirstZoneSize)); Bitmap zoneImg = newDriverZone.ZoneImage; newDriverZone.AddWindow(new DriverPositionWindow(zoneImg, new Rectangle(driverPositionPosition, driverPositionArea))); newDriverZone.AddWindow(new DriverGapToLeaderWindow(zoneImg, new Rectangle(driverGapToLeaderPosition, driverGapToLeaderArea))); newDriverZone.AddWindow(new DriverLapTimeWindow(zoneImg, new Rectangle(driverLapTimePosition, driverLapTimeArea))); newDriverZone.AddWindow(new DriverDrsWindow(zoneImg, new Rectangle(driverDrsPosition, driverDrsArea))); newDriverZone.AddWindow(new DriverTyresWindow(zoneImg, new Rectangle(driverTyresPosition, driverTyresArea))); newDriverZone.AddWindow(new DriverNameWindow(zoneImg, new Rectangle(driverNamePosition, driverNameArea))); newDriverZone.AddWindow(new DriverSector1Window(zoneImg, new Rectangle(driverSector1Position, driverSector1Area))); newDriverZone.AddWindow(new DriverSector2Window(zoneImg, new Rectangle(driverSector2Position, driverSector2Area))); newDriverZone.AddWindow(new DriverSector3Window(zoneImg, new Rectangle(driverSector3Position, driverSector3Area))); MainZone.AddZone(newDriverZone); } Alors que Zone.AddWindow c'est simplement : public virtual void AddWindow(Window window) { Windows.Add(window); } Et windows est simplement une liste. Donc ca ne peut pas \u00eatre ca qui prend du temps. Et les windows que je cr\u00e9\u00e9 ont ca comme code : public DriverPositionWindow(Bitmap image, Rectangle bounds) : base(image, bounds) { Name = \"Position\"; } Sachant que le constructeur de base d'une Window c'est : public Window(Bitmap image, Rectangle bounds) { Image = image; Bounds = bounds; Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, \"eng\", EngineMode.Default); Engine.DefaultPageSegMode = PageSegMode.SingleLine; } Sachant que TesseractEngine est en statique et que donc il ne devrait... OHLLALALALALALALALALA je suis un imb\u00e9cile... J'ai juste \u00e0 changer ce constructeur avec ca: if (Engine == null) { Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, \"eng\", EngineMode.Default); Engine.DefaultPageSegMode = PageSegMode.SingleLine; } ET le loading ne prend plus que 2-300 ms... Bon c'est une tr\u00e8s belle am\u00e9lioration pour pas tr\u00e8s ch\u00e8r mais bon c'est un peu b\u00eate... Bon je pense que 2-300ms c'est une dur\u00e9e correcte surtout que ca n'est appel\u00e9 qu'une fois pour le lancement. On peut passer \u00e0 la suite maintenant. Alors il y a un grand soucis avec la parallellisation de l'OCR... Tesseract n'est pas par d\u00e9faut une classe \"Thread safe\" ce qui veut dire que je ne peut utiliser de parallell.Foreach sur mes windows pour acc\u00e8l\u00e8rer le traitement drastiquement. Je pourrais par exemple avoir une instance de Tesseract par window sauf que cela fait 20 pilotes * 9 windows chacuns ce qui donne 180 instances ce qui n'est tout simplement pas raisonnable. Je vais donc essayer de voir avec l'utilisation de methodes asynchrones qui me permettraient de faire un genre de flux tendu de reconnaissance. J'avoue que la je navigue un peu \u00e0 vue, je me base sur diff\u00e9rentes infos que je trouve sur des sites un peu perdus et sur chatGPT, j'esp\u00e8re que j'arriverai \u00e0 trouver une solution car 10 secondes de reconnaissance c'est vraiment beaucoup trop. Alors le soucis avec un Engine unique entre toutes les windows c'est qu'il n'est pas possible de process plusieurs images \u00e0 la fois. Je vais donc retirer l'engine unique pour voir si en cr\u00e9er un par window me permet de passer en multithreading. La grande question sera : Est-ce que les ressources suppl\u00e9mentaires que vont prendre la cr\u00e9ation de tous ces engines va compenser enti\u00e8rement le temps gagn\u00e9 avec la paralellisation. Pour stocker les donn\u00e9es dans un premier temps je vais cr\u00e9er un objet DriverData. Ce qu'il y a de pratique avec ca, c'est que je pourrais ajouter du code de v\u00e9rification de certaines donn\u00e9es directement dedans avant de les donner \u00e0 la suite du programme. Et on peut m\u00eame imaginer une impl\u00e9mentation d'une liste de DriverData pour avoir l'historique. Ce qui serait cool ca serait de grouper toutes ces data avec un num\u00e9ro de tour. Placer ensuite la liste de Data dans une DB serait ainsi super simple. Mais il va falloir savoir quoi mettre, quelles infos sont redondantes et prendre en compte le fait que un tour affich\u00e9 sur la page de la F1TV n'est accompli que par certains des premiers pilotes. D'autres pilotes peuvent \u00eatre dans des tours pr\u00e9c\u00e9dents si ils ont du retard. Il faudra r\u00e9fl\u00e9chir \u00e0 cela quand je viendrai au mod\u00e8le. Bon pour y arriver j'ai du faire de gros changements et le r\u00e9sultat n'est peut-\u00eatre pas aussi cool que ce que j'aurais voulut... Voici un petit point sur les performances maintenant J'ai \u00e9galement d\u00e9sactiv\u00e9 le dump d'images. Pour le moment j'ai tout mis en commentaire mais cela pourrait \u00eatre int\u00e9ressant de faire en sorte de pouvoir l'activer en changeant une ou deux variables Au d\u00e9marrage : Loading - 113ms Splitting d'images - 14ms Ocr - 7s En passant d'une image \u00e0 l'autre : Loading - 113ms Splitting d'images - 13ms Ocr - 5s Alors clairement les stats montrent qu'il y a eu un changement mesurable mais bon je pensais pouvoir en gagner un peu plus... Je soupconne la cr\u00e9ation d'engines d'\u00eatre \u00e0 l'origine de ces performances presque d\u00e9cevantes. Autre soucis, il semble que plus je change d'image plus la detection est lente et plus je consomme de RAM. Il va falloir que je travaille encore un peu. J'ai tent\u00e9 de mettre un stopwatch sur une des cr\u00e9ations d'engine Tesseract et le r\u00e9sultat me parait fou... Plus d'une seconde c'est dingue. J'ai test\u00e9 dans d'autres endroits du code et effectivement il semble que la cr\u00e9ation d'un engine prenne entre une et deux secondes ce qui est une ETERNITEE what ! Donc il faut optimiser tout ca. Une id\u00e9e serait de d\u00e9composer le threading mais cela me demanderait un gros refactor et je n'ai pas envie d'en refaire un la... Sinon, une fois qu'ils sont cr\u00e9\u00e9s ils ne prennent pas de temps du tout. Cr\u00e9er une fois tous les engines et ensuite les utiliser pourrait \u00eatre une bonne id\u00e9e. Cela prendrait longtemps au load mais ensuite les reconnaissances devraient \u00eatre super rapides. Ok alors ca c'est d\u00e9ja plus ce \u00e0 quoi je m'attendais ! On est de nouveau \u00e0 plus de 10s de loading time mais on est descendu \u00e0 deux secondes par OCR. (Bon autre soucis, l'utilisation de la RAM est ridicule plus de 2gb mais ce qui m'inqui\u00e8te c'est que j'ai l'impression qu'elle augmente plus on change d'image) J'ai r\u00e8gl\u00e9 (en partie) le soucis en obligeant le GC (Garbage Collector) \u00e0 collecter apr\u00e8s chaque detection. m\u00eame apr\u00e8s 50 detections l'utilisation de la ram se stabilize autour des 2GB. Bon en paralellisant la cr\u00e9ation des Engines le soucis c'est que cela demande d'allouer beaucoup trop de m\u00e9moire d'un coup alors le programme se fige pendant genre cinq secondes avant de tout cr\u00e9er. Du coup m\u00eame si la cr\u00e9ation est plus rapide, on se retrouve avec un temps total plus long... Je pense que l'on va devoir se contenter de ces dix secondes. Bon la j'allais tenter de faire la documentation mais je viens de me rendre compte que la detection de temps au tour est pas vraiment encore id\u00e9ale... J'ai r\u00e9ussi \u00e0 changer un petit peu le programme de reconnaissance pour rendre la reconnaissance un peu meilleure mais cela a drastiquement augment\u00e9 le temps requis pour d\u00e9coder... On arrive \u00e0 3.5 secondes. Je vais tenter de rajouter un peu de parralell processing sur les boucles de traitement voir si cela peut aider. Alors effectivement cela aide pas mal, on arrive maintenant \u00e0 faire une detection presque tout le temps en dessous de la seconde. Et j'ai aussi du changer un peu le fonctionnement de la detection des Temps au tour. Et voila je pense que je vais m'arr\u00eater la pour la partie d\u00e9codage. Je ne pense pas que je peux facilement faire mieux que ca et il faut que j'avance dans d'autres parties du projet. Je vais pouvoir commencer \u00e0 documenter un peu toute la partie OCR. Il faut que je prenne le temps de le faire bien car c'est la partie la plus int\u00e9ressante du projet et ou je pense que j'aurai le plus essay\u00e9 de choses qui vallent le coup d\u00eatres racont\u00e9es. J'ai aussi pass\u00e9 pas mal de temps sur le poster du projet. J'avais fait des croquis au crayon de ce \u00e0 quoi je pensais, cependant apr\u00e8s de longues discussions avec M.Garcia ils n'\u00e9taient pas forc\u00e9ment tr\u00e8s bons car ils ne repr\u00e9sentent pas assez bien le fonctionnement du projet et sont un peu trop marketings. Du coup j'ai fait une premi\u00e8re version au propre : \"Poster V1\" Mais je n'\u00e9tais pas forc\u00e9ment content du r\u00e9sultat et il manquait des choses je trouve comme par exemple l'utilisation de Selenium. J'ai donc repass\u00e9 des heures \u00e0 faire une seconde version : \"Poster V2\" La police d'\u00e9criture n'est pas encore la bonne mais cela va venir. Mais je pr\u00e9f\u00e8re d\u00e9ja beaucoup cette version \u00e0 la premi\u00e8re. Je ne sais pas encore si la version finale sera une version plus travaill\u00e9e de ce poster ou compl\u00eatement autre chose mais pour l'instant je suis \u00e0 peu pr\u00e8s content de cette version. Je le trouve un tout petit peu trop brouillon ou avec trop d'infos mais il m'a \u00e9t\u00e9 de nombreuses fois reproch\u00e9 de ne pas assez montrer le fonctionnememt interne et je ne peux pas faire plus simple. L'ajout des nombres pour compartimenter le projet ajoute de la structure mais je me demande si cela suffit. Maintenant que je suis \u00e0 peu pr\u00e8s content de mon code pour l'OCR je vais commencer sa documentation. (Uniquement son fonctionnement interne pas comment s'en servir car cela va changer) Bon j'ai cr\u00e9\u00e9 u nouveau projet selenium mais m\u00eame avec les bonnes libraries je n'arrivais pas \u00e0 faire fonctionner firefox j'avais toujours une erreur \"OpenQA.Selenium.WebDriverException: 'Cannot start the driver service on http://localhost:51481/'\" et j'ai pu r\u00e8gler le probl\u00e8me en t\u00e9l\u00e9chargeant directement le gecko driver depuis le git https://github.com/mozilla/geckodriver/releases et utiliser le fichier directement dans le service : var service = FirefoxDriverService.CreateDefaultService(AppDomain.CurrentDomain.BaseDirectory+@\"geckodriver-v0.27.0-win32\\geckodriver.exe\"); FirefoxOptions options = new FirefoxOptions(); var driver = new FirefoxDriver(service,options); Le seul probl\u00e8me c'est que du coup il faut tout le temps d\u00e9placer le fichier dans le dossier bin si je clone le projet. Il faudra faire un installeur dans la version finale qui s'occupe de tout je pense. Je me suis dit que j'allais garder la doc pour le retour des vacances quand j'aurai un bureau un clavier et un setup complet un peu propres. Bon il va falloir que je parle de la r\u00e9cup\u00e9ration de cookie. J'ai d\u00e9ja pu travailler lors d'un poc sur la meilleure facon de prendres des screenshots de la F1TV : Avoir une page chrome ouverte avec le feed en plein \u00e9cran et un programme qui prend des captures d'\u00e9crans. Avoir une cam\u00e9ra qui prend en photo l'\u00e9cran au cas ou chrome et Firefox emp\u00eachent la prise de captures d'\u00e9crans. R\u00e9cup\u00e8rer directement le feed en faisant du reverse engeneering de la plateforme. Simuler un chrome en background qui prenne des screenshot sans qu'on aie \u00e0 le voir. Dans toutes ces options, je dirais que la pire \u00e9tait celle de la cam\u00e9ra qui filme l'\u00e9cran, mais \u00e0 l'\u00e9poque c'\u00e9tait encore envisageable comme solution de dernier recours. Le soucis de cette solution c'est l'horreur que serait la partie OCR avec une image de tr\u00e8s mauvaise qualit\u00e9. Une autre option qui m'aurait vraiment emb\u00eat\u00e9 aurait \u00e9t\u00e9 de devoir garder une page de Chrome ou Firefox ouverte quelque part sur un \u00e9cran pour que le programme puisse prendres des captures d'\u00e9crans. C'est de loin l'option la plus simple et la plus logique mais elle poss\u00e8de pour moi de tr\u00e8s gros points noirs : On ne peut pas certifier l'int\u00e9grit\u00e9 des donn\u00e9es car l'utilisateur a le contr\u00f4le total sur le feed. Il peut mettre pause, avancer, reculer, tout casser sans faire expr\u00e8s en ouvrant autre chose sur son ordi qui se mette pile devant. Bref c'est un peu bancale. Et surtout on bloque une partie non significative de l'\u00e9cran de l'utilisateur avec des infos redondantes. Et je peux vous dire que quand je commente la F1 j'ai besoin de beaucoup d'informations et que chaque centim\u00e8tre d'\u00e9cran est crucia\u00e9 ! Alors avoir un \u00e9cran complet bloqu\u00e9 est juste un point bloquant qui m'emp\u00eacherait d'utiliser l'app aussi bonne soit-elle dans ses pr\u00e9dictions. Mais bon si aucune autre methode ne fonctionne ce qui est bien c'est que celle la est plut\u00f4t simple \u00e0 mettre en place. Ensuite reverse engeneer le feed serait l'option la plus classe, cependant c'est la plus complexe et la plus bancale au niveau l\u00e9gal haha. L'id\u00e9e serait de r\u00e9cup\u00e8rer le lien vers le broadcast g\u00e9n\u00e9ral et de comprendre comment il fonctionne pour le d\u00e9coder nous m\u00eame pendant un Grand Prix. Seuls soucis : Il n'est pas possible de faire des tests en dehors des periodes de Grand Prix (Et je rappelle que c'est des p\u00e9riodes ou je travaille en plus) Difficile de faire un syst\u00e8me qui marche pareil pour les rediffusions et les lives. (En effet les liens des rediff sont beaucoup plus simple \u00e0 r\u00e9cup\u00e8rer mais ne fonctionnent pas du tout pareil et pour tester l'app il est essentiel de pouvoir s'entrainer sur des anciens Grand Prix) Dernier GROS soucis, je ne sais tout simplement pas faire ca lol. Je ne sais pas comment faire. Peut-\u00eatre que avec des profs qui m'aident et chat gpt ainsi qu'internet je pourrais potentiellement n\u00e9gocier un truc mais c'est hautement improbable et cela serait une perte de temps folle si je n'y arrive pas. Derni\u00e8re option que je trouve la plus s\u00e9duisante. Simuler une instance de Chrome ou de Firefox (Le soucis avec chrome c'est qu'il impl\u00e9mente l'utilisation de DRM dans les vid\u00e9os qui fait qu'il est tr\u00e8s difficile de passer outre la s\u00e9curit\u00e9 avec un bot) pour ensuite prendre des captures d'\u00e9crans automatiquement. Cette solutions offre pleins d'avantages : Pas de place prise sur l'\u00e9cran L'int\u00e9grit\u00e9 des donn\u00e9es est assur\u00e9e car c'est le programme qui d\u00e9cide d'ou partir et de si il met pause ou non C'est une option complexe mais beaucoup moins que le reverse engeneering Elle permet de ne demander presque aucun input de la part de l'utilisateur. Mais elle pose quelques probl\u00e9matiques : Comment se connecter automatiquement sans \u00eatre detect\u00e9 par un Bot et sans demander \u00e0 l'utilisateur ses identifiants (Pour des raisons \u00e9videntes qui sont : QUI VA METTRE SES IDENTIFIANTS SUR UNE VIEILLE APP COMME LA MIENNE??) Comment faire en sorte que le programme prenne les meilleures captures dans la meilleure qualit\u00e9 et en plein \u00e9cran. Mais j'ai d\u00e9cid\u00e9 de partir sur cette option. Pour ce faire j'utilise Selenium. J'ai pu tester Puppetteer Sharp et m\u00eame si dans un premier temps j'ai pu avancer asez vite, malheureusement il y a des bugs qui rendent son utilisation impossible dans notre contexte. J'ai donc d\u00e9cid\u00e9 de tout faire en utilisant un portage de Selenium dans mon programme. Voici un exemple de code qui va ouvrir FireFox et qui va lancer un RickRoll var service = FirefoxDriverService.CreateDefaultService(AppDomain.CurrentDomain.BaseDirectory+@\"geckodriver-v0.27.0-win32\\geckodriver.exe\"); service.Host = \"127.0.0.1\"; service.Port = 5555; FirefoxOptions options = new FirefoxOptions(); options.AddArgument(\"--disable-headless\"); var driver = new FirefoxDriver(service,options); driver.Navigate().GoToUrl(\"https://www.youtube.com/watch?v=dQw4w9WgXcQ&autoplay=1&mute=1\"); Dans cet exemple on d\u00e9sactive le \"Headless\" pour qu'on puisse voir ce que fait l'app car sinon tout est invisible. Alors dans les faits la vid\u00e9o youtube ne se lance pas du tout car il y a des pubs et des prompts de cookies que l'on doit accepter etc... ce qui montre les diff\u00e9rents challenges que l'on va devoir surmonter pour vraiment faire ce que l'on veut. Mais un petit d\u00e9tail extr\u00eamement important, la F1TV est un programme payant un peu comme netflix. Ce qui veut dire que pour acc\u00e8der au contenu il faut \u00eatre connect\u00e9. Sauf que une instance de firefox cr\u00e9\u00e9 par Selenium est comme une page de naviguation priv\u00e9e, ce qui veut dire que si on va sur la page de la F1TV on est pas connect\u00e9s. Je pourrais tout \u00e0 fait demander \u00e0 l'utilisateur de me donner ses identifiants pour que j'aille ensuite automatiquement me connecter sauf que cela pose deux soucis: Personne ne voudra mettre ses identifiants sur mon programme La page de login de la F1TV a \u00e9t\u00e9 prot\u00e8g\u00e9e avec la meilleure technologie de detection de bots que je connaisse. Presque aucun site n'arrive \u00e0 me detecter sauf eux ! Donc c'est tout simplement impossible d'utiliser cette technique. Ensuite je me suis rappel\u00e9 que ce que la page stocke pour me permettre de rester connect\u00e9 ce sont des cookies. Et si je mets le bon cookies dans Selenium alors je serai connect\u00e9. Dans un premier temps je voulais faire un syst\u00e8me ou l'utilisateur irait prendre dans son chrome son cookie et le copie colle dans mon programme mais c'est immonde. C'est alors que vient la partie r\u00e9cup\u00e8ration de cookies ! Tous les cookies de chrome sont stock\u00e9s dans une base de donn\u00e9es SQLITE. On pourrait se dire Banco il suffit d'aller dedans et de retrouver tous les cookies et se connecter. Sauf que, pas b\u00eates, les \u00e9quipes de chrome ont d\u00e9cid\u00e9 que c'\u00e9tait une bonne id\u00e9e d'encoder les cookies pour que tout le monde ne puisse pas venir y mettre son nez... En effet les cookies peuvent contenir des informations importantes. Cela fait que pour utiliser ces cookies il faut pouvoir les d\u00e9coder. Mon hypoth\u00e8se a \u00e9t\u00e9 que si ces cookies peuvent \u00eatre lus par Chrome m\u00eame hors connexion, c'est que la cl\u00e9 de d\u00e9codage existe sur l'appareil et qu'il suffit de la trouver. ET C'EST LE CAS! Apr\u00e8s pas mal de recherches j'ai pu voir que la cl\u00e9 de d\u00e9codage existe bel et bien et qu'il suffit de la d\u00e9coder en utilisant la librairie DPAPI pour la lire. Avec cette cl\u00e9 on peut ensuite d\u00e9coder les cookies et leurs valeurs ce qui veut dire qu'il est th\u00e9oriquement possible d'automatiser le processus sans que l'utilisateur n'aie rien \u00e0 faire. J'ai d\u00e9cid\u00e9 de faire la partie r\u00e9cup\u00e8ration en python pour deux raison : Je n'arrivais pas \u00e0 trouver une bonne impl\u00e9mentation de DPAPI en C# qui me permettait de d\u00e9coder la cl\u00e9. Il existe beaucoup plus de documentation en Python pour ce qui est de la cryptographie et donc si Chrome change de fonctionnement il sera beaucoup plus simple de changer cette partie en particulier sans avoir \u00e0 recompiler le code C#. J'ai donc avec l'aide d'internet et de ChatGPT cr\u00e9\u00e9 ce script : def get_master_key(): with open( os.getenv(\"localappdata\") + \"\\\\Google\\\\Chrome\\\\User Data\\\\Local State\", \"r\" ) as f: local_state = f.read() local_state = json.loads(local_state) master_key = base64.b64decode(local_state[\"os_crypt\"][\"encrypted_key\"]) master_key = master_key[5:] # removing DPAPI master_key = win32crypt.CryptUnprotectData(master_key, None, None, None, 0)[1] print(\"MASTER KEY :\") print(master_key) print(len(master_key)) return master_key def decrypt_payload(cipher, payload): return cipher.decrypt(payload) def generate_cipher(aes_key, iv): return AES.new(aes_key, AES.MODE_GCM, iv) def decrypt_password(buff, master_key): try: iv = buff[3:15] payload = buff[15:] cipher = generate_cipher(master_key, iv) decrypted_pass = decrypt_payload(cipher, payload) decrypted_pass = decrypted_pass[:-16].decode() # remove suffix bytes return decrypted_pass except Exception: # print(\"Probably saved password from Chrome version older than v80\\n\") # print(str(e)) return \"Chrome < 80\" master_key = get_master_key() cookies_path = Path( os.getenv(\"localappdata\") + \"\\\\Google\\\\Chrome\\\\User Data\\\\Default\\\\Network\\\\Cookies\" ) if not cookies_path.exists(): raise ValueError(\"Cookies file not found\") with sqlite3.connect(cookies_path) as connection: connection.row_factory = sqlite3.Row cursor = connection.cursor() cursor.execute(\"SELECT * FROM cookies\") with open('cookies.csv', 'a', newline='') as csvfile: fieldnames = ['host_key', 'name', 'value', 'path', 'expires_utc', 'is_secure', 'is_httponly'] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) if csvfile.tell() == 0: writer.writeheader() for row in cursor.fetchall(): decrypted_value = decrypt_password(row[\"encrypted_value\"], master_key) writer.writerow({ 'host_key': row[\"host_key\"], 'name': row[\"name\"], 'value': decrypted_value, 'path': row[\"path\"], 'expires_utc': row[\"expires_utc\"], 'is_secure': row[\"is_secure\"], 'is_httponly': row[\"is_httponly\"] }) print(\"Finished CSV\") Ce programme va faire tout ce que j'ai expliqu\u00e9 et va ensuite stocker les r\u00e9sultats dans un CSV pour qu'il soit facile d'y acc\u00e8der depuis le C#. Alors oui cela pose certaines questions de s\u00e9curit\u00e9. Car en effet je prend tous les cookies, les d\u00e9code et les stocke. Ce qui veut dire que je pourrais tout \u00e0 fait envoyer ces donn\u00e9es quelque part, par exemple un compte Netflix, et me rincer. Si je devais rendre le projet ouvert au public je pense qu'il faudra que cela soit mentionn\u00e9 clairement et que le projet soit open source pour que les utilisateurs puissent verifier que je ne fais pas ca. Maintenant de l'autre c\u00f4t\u00e9 j'ai juste \u00e0 lire le CSV et le tour est jou\u00e9 ! (Trouver cette solution m'a pris une semaine de vacances \u00e0 l'\u00e9poque) Bon j'ai r\u00e9ussi \u00e0 faire le programme se connecter et naviguer etc.. Par contre quelque chose que j'ai voulu ajouter et qui m'a pris pas mal de temps c'est de faire en sorte de pouvoir selectionner la qualit\u00e9. Pour changer la qualit\u00e9 du feed il faut cliquer sur settings et ensuite prendre le menu deroulant et selectioner 1080p. Le soucis c'est le que la value du select est jamais la m\u00eame. Elle commence toujours pas \"1080_\" mais ensuite ca peut \u00eatre \"1080_45930285\" ou \"1080_56801\" la suite est apparemment random. J'ai donc du utiliser ce code pour le selectioner quand m\u00eame : IWebElement settingsButton = driver.FindElement(By.ClassName(\"bmpui-ui-settingstogglebutton\")); settingsButton.Click(); IWebElement selectElement = driver.FindElement(By.ClassName(\"bmpui-ui-videoqualityselectbox\")); SelectElement select = new SelectElement(selectElement); IWebElement selectOption = selectElement.FindElement(By.CssSelector(\"option[value^='1080_']\")); selectOption.Click(); Sauf que pour que cela marche je dois avant cliquer sur le bouton des settings le probl\u00e8me c'est qu'il est invisible alors on doit le faire apparaitre. J'ai tent\u00e9 de le faire aparaitre en bougeant la souris, en cliquant \u00e0 un endroit pr\u00e9cis, impossible de le faire marcher correctement. Puis j'ai eu l'id\u00e9e de mettre pause en envoyant un appui sur la touche Espace et ca a permit de d\u00e9couvrir le bouton et permettre qu'on clique dessus. Ca peut paraitre tout b\u00eate mais rien que ca, ca m'a pris un temps consid\u00e9rable. Bon pour ce qui est du timecode de la vid\u00e9o. Je pense qu'il serait trop complexe de faire en sorte que selenium change le slider de progression de la vid\u00e9o. Alors j'ai fait quelques tests et apparemment, si on quitte la F1TV sur un timecode de la vid\u00e9o que on donne au programme, comme il r\u00e9cup\u00e8re tous les cookies de la F1TV il commencera de la. Donc si on veut utiliser le programme avec des Grand Prix ayant d\u00e9ja eu lieu, on peut le faire, seulement il faudra juste au pr\u00e9alable avoir choisit le bon timecode dans le page de la F1TV avant de le lancer. Ce qui est int\u00e9ressant c'est que la page de la F1TV ressemble \u00e0 ca au d\u00e9part : \"Empty F1TV\" Je pense qu'une bonne id\u00e9e serait de dire au programme que c'est la grille de d\u00e9part et ensuite d\u00e8s qu'il d\u00e9tecte un secteur il sait que la course a commenc\u00e9. Lundi 24 Avril 2023 Aujourd'hui c'est jour de documentation. J'ai pas mal travaill\u00e9 pendant les vacances mais je n'ai pas encore pu faire de vraie documentation correcte du fonctionnement. Du coup je vais m'en charger aujourd'hui et peut-\u00eatre un peu demain. Ok normalement je ne devrais faire que de la documentation mais je ne peux pas passer \u00e0 cot\u00e9 de ca... Le probl\u00e8me que j'ai avec les pneus ou parfois il d\u00e9tecte un H au lieu d'un '11' et ce genre de choses c'est \u00e0 cause de ma methode \"RemoveBG\" Qui va retirer tous les pixels plus sombres que le background. Sauf que cela va aussi retirer des pixels dans le chiffre lui m\u00eame et qui va donc defigurer les 11 : \"diformed 11\" \"diformed 11\" J'ai r\u00e9ussi \u00e0 les changer en : \"less diformed\" \"less diformed\" Mais au final cela n'a pas augment\u00e9 la pr\u00e9cision de la reconnaissance. Je pense que je vais donc devoir encore changer. Je pense que une bonne facon de trouver serait dabord de trouver la couleur du pneu. Et si il n'y a pas assez de couleur alors c'est que le pneu contient une lettre. Le but est d'arr\u00eater de chercher des lettres ou des chiffres. Comme ca les 11 arr\u00eateront d'\u00eatre pris pour des 'H' En fait on peut faire encore plus simple que ca. On peut simplement regarder la couleur dominante et determiner le pneu. En effet m\u00eame si il y a une lettre sur fond noir pour d\u00e9crire le pneu, mon methode de r\u00e9cup\u00e8ration de la couleur dominante ommet les pixels trop noirs alors il est quand m\u00eame possible de determiner le type de pneus. Et tout simplement si il n'arrive pas \u00e0 lire le chiffre c'est que c'est une lettre et que donc on est \u00e0 0 tours. Cela marche plut\u00f4t bien et cela simplifie pas mal le processing. Voila, la je vais me remettre \u00e0 la documentation sinon je vais encore prendre du retard. Mardi 25 Avril 2023 Encore une fois j'ai pris du temps de doc pour changer des choses sur la partie OCR. Mais en m\u00eame temps en documentant je vois des choses que j'ai soit mal fait soit que je pourrais faire mieux en changeant tr\u00e8s peu de choses. J'\u00e9sp\u00e8re que les changement que j'ai fait vont aider au moins \u00e0 la coh\u00e9rence du code et un peu pour les performances. Il semble que dans les conditions que j'ai test\u00e9 le nombre de tour soit plut\u00f4t fiable mais je pense que je devrai faire un peu de travail en aval dans la r\u00e9cup\u00e9ration de ces donn\u00e9es car je sens que cela va poser probl\u00e8me quelques fois. Je pense que en utilisant bien l'historique on peut potentiellement se passer de l'utilisation de ce chiffre pas toujours compl\u00eatement fiable. Mais sinon aujourd'hui c'est encore une fois un gros jour de doc. J'essaie d'expliquer les diff\u00e9rents proc\u00e9d\u00e9s avant de les oublier. J'essaie aussi de donner un maximum d'exemples sous formes de photos interm\u00e9diaires mais ca me prend pas mal de temps car il faut que j'ajoute un peu partout dans le code des lignes pour sortir des images interm\u00e9diaires. En plus de la documentation je me suis aussi beaucoup occup\u00e9 de nettoyer mon code et je suis assez content par ce que m\u00eame en ayant du rajouter des couches de complexit\u00e9 pour mieux reconnaitres les temps au tour j'arrive \u00e0 un temps de processing parfois en dessous des 2 secondes ce que je trouve honorable. Quand j'aurai finit de nettoyer tous mes fichiers je ferai une release sur gitea et ce sera la version que j'utiliserai quand je voudrai faire un merge avec les autres parties du projet. J'ai beaucoup beaucoup boss\u00e9 aujourd'hui et je sui bien mort. Faire autant de documentation et de nettoyage de code c'est pas forc\u00e9ment bon pour le cerveau je crois. J'ai besoin d'une sieste. Demain je pense que je vais commencer \u00e0 avancer sur la partie r\u00e9cup\u00e8ration des images. Je sais que la je fais un peu passer les tests \u00e0 la trappe mais d\u00e9ja j'en ai fait tout le long du d\u00e9veloppement de OCR_DECODE et il faut vraiment que j'avance, quitte \u00e0 revenir dessus quand j'aurai merge les deux projets ensemble. 26 Avril 2023 Aujourd'hui je vais devoir m'occuper de la partie r\u00e9cup\u00e9ration des images. J'ai d\u00e9ja eu l'occasion d'avancer sur ce projet pendant mopn poc et mes vacances. Donc la le but ca va \u00eatre de voir ce qui manque comme v\u00e9ritables features et ensuite je vais pouvoir m'occuper de la vue et de son int\u00e9gration avec le d\u00e9codage. Ok donc maintenant que j'au un programme qui arrive \u00e0 prendre des images depuis la F1TV correctement et en bonne r\u00e9solution. Je pense qu'il est temps de passer \u00e0 l'impl\u00e9mentation de la Forme que ca va prendre. C'est important de se poser au moins cinq minutes la question de comment je pr\u00e9vois de faire car m\u00eame si ca n'est pas la version finale, cette derni\u00e8re prendra tr\u00e8s fort inspiration du desing que je vais faire. Dans cette form j'aurais besoin de : Pouvoir selectionner un Grand Prix en ins\u00e8rant l'URL du feed. Pouvoir lancer la calibration si besoin Indiquer le titre et la date du Grand Prix Indiquer si le Grand Prix vient de commencer ou si il y a d\u00e9ja un certain nombre de tours lanc\u00e9s. Et c'est \u00e0 peu pr\u00e8s tout en fait... J'ai tellement pouss\u00e9 pour avoir un programme qui fait tout tout seul que il ne me faut pas grand chose de plus. Je pense que ce qui serait pas mal ca serait du coup d'utiliser ce temps pour bien impl\u00e9menter la calibration qui elle aura besoin d'une UI un peu plus bal\u00e8ze. On pourrait m\u00eame imaginer que la calibration fasse partie int\u00e9grante des settings... Ca serait peut-\u00eatre bien que quand l'application se lance on se retrouve sur la page principale d'affichage de donn\u00e9es et qu'on puisse simplement cliquer sur la page options qui contient la page calibration et qui permet de rentrer les infos du Grand Prix. Je pense que je vais faire ca. Voici l'interface que j'ai d\u00e9velopp\u00e9e pour regrouper tout ca : \"Screen\" La police le style le placement et les couleurs ne sont pas d\u00e9finitfs, cependant je pense que c'est un bon d\u00e9but. Le but maintenant va \u00eatre de permettre de faire fonctionner la calibration et la r\u00e9cup\u00e8ration d'images. Si j'arrive \u00e0 faire fonctionner ces deux choses sur un m\u00eame projet avant la fin de la semaine cela serait super ! Bon J'ai pu avancer sur l'int\u00e8gration de Selenium mais cela prend un peu de temps car je veux impl\u00e9metner un moyen de pouvoir prendre une Screenshot \u00e0 nimporte quel moment et pas juste en boucle. Demain je finis de faire fonctionner ca et ensuite je commence le cablage du reste. Jeudi 27 Avril 2023 C'est assez dur de faire l'importation car il y a des petites diff\u00e9rences qui obligent \u00e0 presque tout r\u00e9\u00e9crire. En fait le programme de calibration avait d\u00e9ja impl\u00e9ment\u00e9 la fonction de Windows et de Zones mais il fonctionnait juste assez diff\u00e9remment pour qu'il faille tout refaire. La je suis en train de perdre \u00e9norm\u00e9ment de temps \u00e0 cause d'un soucis de coordonn\u00e9es. J'ai repris le code de la calibration pour detecter ou l'utilisateur a cliqu\u00e9 pour cr\u00e9er les zones. Cependant, je n'arrive pas \u00e0 le faire fonctionner correctement. La zone est tout le temps d\u00e9cal\u00e9e en haut et en bas mais pas de la m\u00eame facon. En haut, la valeur Y est trop grande alors que en bas la valeur Y est trop petite... Je ne comprends pas bien pourquoi. Si c'\u00e9tait un simple d\u00e9calage cela ne serait pas compliqu\u00e9 \u00e0 g\u00e8rer mais la... J'ai un soucis \u00e9galement avec la r\u00e9solution des screenshots que je r\u00e9cup\u00e8re en full Headless. Voici un exemple de r\u00e9solution que j'arrive \u00e0 r\u00e9cup\u00e8rer sans le headless : \"High Res\" \"Low Res\" Il y a clairement un soucis et le probl\u00e8me c'est que avec une r\u00e9solution pareille, impossible de faire une reconnaissance correcte. BON J?EN PEUX PLUS LA. Ca fait des heures que je bosse sur ce probl\u00e8me d\u00e9bile et impossible de trouver une solution. J'ai essay\u00e9 cinq facons de forcer le browser headless a prendre une plus haute r\u00e9solution aucune ne fonctionne je ne comprends pas. A chaque fois que je me retrouve avec une r\u00e9solution de 1366 x 768 Ou une variante de basse r\u00e9solution du style. J'en peux plus je ne trouve aucune r\u00e9ponse sur internet ni m\u00eame avec chatGPT. Super... La seule chose que j'ai pu faire qui change quelque chose fait que les images font maintenant du 926x517... j'ai un peu envie de commentre un crime de guerre au plus vite. Vendredi 28 Avril 2023 Une des solutions que je n'ai pas encore essay\u00e9 est de changer ma version de GeckoDriver. Sauf que ca m'oblige \u00e0 changer les versions de mes libraries ce qui est tr\u00e8s p\u00e9nible, je vais continuer le debugging dans le projet Selenium_clean. Il faut savoir que la librairie de Selenium que j'utilise est bloqu\u00e9e en 0.27 ce qui fait que je ne peux utiliser qu'une version obsol\u00e8te du Gecko Driver. J'ai tent\u00e9 de changer vers une version en 64 bits du GeckoDriver 0.27 mais pareil, je me retrouve toujours avec des images de M. J'essaie toutes les solutions que je trouve sur internet aucune ne convient c'est infernal. J'essaie de changer la r\u00e9solution DPI, j'essaie de changer les param\u00eatres par d\u00e9faut des player de Firefox, j'essaie de changer la r\u00e9solution pendant et au d\u00e9but de l'execution IMPOSSIBLE DE FAIRE MARCHER CETTE MERDE C'EST PAS POSSIBLE !!! J'ai essay\u00e9 avec chrome mais je ne peux pas l'utiliser car les DRM m'emp\u00eacheront de prendre des screenshot du flux vid\u00e9o. J'ai essay\u00e9 de faire tourner avec edge mais edge ne peut pas tourner en headless. JE VAIS DEVENIR FOUF FPWQOVMQEKOVNVIBDBJDAIVOBI. ET MAINTENANT JE N'ARRIVE PLUS A FAIRE DE PROJET AVEC SELENIUM VOIWQNV(UEWQBVU)WEQN=OEJNIVIUWVBWUEV ON CHERCHE A ME FAIRE PETER UN PLOMB C'EST PAS POSSIBLE GIWEGUWEQN VOICI UN EXEMPLE DU CODE QUE JE DEMANDE A UN NOUVEAU PROJET AVEC EXACTEMENT LES MEMES LIBRARIES INSTALLEES : // Create a new FirefoxDriver instance IWebDriver driver = new FirefoxDriver(); // Navigate to the specified URL driver.Navigate().GoToUrl(\"https://www.example.com\"); // Do something with the driver (e.g., find elements or take screenshots) // Quit the driver driver.Quit(); Je ne demande que ca ET MEME CA CA NE VEUT PAS FONCTIONNER VOIWENB)IWUQENV Oui je suis un peu \u00e9nerv\u00e9 ca se voit? A bon? Et maintenant NUGGET ne fonctionne plus... j'en peux plus la. Je ne peux plus t\u00e9l\u00e9charger de librairie sur aucun de mes projets... J'ai tent\u00e9 de supprimer le fichier de config et red\u00e9marrer Visual Studio mais cela ne fait rien. J'ai aussi tent\u00e9 de faire un 'nugget restore' toujours rien. Bon apparemment je ne suis pas le seul qui ne peut pas acc\u00e8der \u00e0 Nuget donc bon c'est pas juste chez moi qu'il y a un soucis. Mais m\u00eame en mettant ma 4G pour me connecter, je n'arrive pas \u00e0 acc\u00e8der \u00e0 certains sites y compris Nuget et je ne peux pas download de librairies... Je ne comprends pas ce qui se passe et du coup je ne peux juste pas bosser... J'ai red\u00e9marr\u00e9 trois fois mon pc et visual studio, j'ai essay\u00e9 de changer mes settings DNS etc... impossible de bosser. Je crois que je n'aurais pas du me reveiller aujourd'hui. Bon je vais tenter d'avancer sur mon poster en attendant que le r\u00e9seau soit en meilleur \u00e9tat. Lundi 1 Mai 2023 Bon je bosse depuis chez moi donc j'esp\u00e8re que Nuget va mieux fonctionner. Apr\u00e8s un weekend \u00e0 r\u00e9fl\u00e9chir au sujet de cette resolution je me suis dit deux choses. La seule personne sur internet que j'ai vu avoir le meme soucis avait une r\u00e9solution de 1920x1200 comme moi. Cela veut donc s\u00fbrement dire que le soucis vient de cette r\u00e9solution de laptop comme moi. Si vraiment je n'arrive pas dans un premier temps \u00e0 faire fonctionner le Headless correctement, je peux toujours laisser la page de c\u00f4t\u00e9 et m'occuper du reste du programme. Certes ca serait vraiment infernal d'avoir \u00e0 garder une page chrome ouvert en tous temps et en plus elle doit \u00eatre en plein \u00e9cran mais bon... Si il n'y a vraiment pas d'autres solutions malheureusement je serai bien oblig\u00e9. BON ! JE N'ARRIVE MEME PLUS A FAIRE UN PROJET QUI UTILISE SELENIUM ET QUI MARCHE JE VAIS FAIRE BR\u00dbLER GENEVE. C'est pas possible serieux, je ne comprends pas j'essaie tout ce que je trouve et impossible de juste lancer firefox c'est du grand nimporte quoi. Je prend les m\u00eame putain de librairies que sur les autres projets les m\u00eames versions, je prend le m\u00eame exact code. Sur le nouveau projet impossible de le faire fonctionner. Je commence \u00e0 croire que on essaie de me faire p\u00eater un cable. Du coup dans un \u00e9lan de d\u00e9sespoir je vais tenter de passer sur une autre librairie qui avec un peu de chance marche et en plus me permettrais de prendre des foutues screenshot dans le bon format. Les deux seules librairies qui pourraient potentiellement faire l'affaire sont les librairies : PhantomJS CefSharp Je vais les tester et simplement prier pour qu'elles fonctionnent et que je puisse faire ce que je veux avec. Alors pour le moment avec CEFSharp j'arrive \u00e0 lancer une instance de chrome et prendre une screenshot avec ce code : CefSettings settings = new CefSettings(); settings.CachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), \"CefSharp\\\\Cache\"); // Set cache path settings.LogSeverity = LogSeverity.Disable; // Disable logging Cef.Initialize(settings); // Initialize CEF using (var browser = new ChromiumWebBrowser(\"www.google.com\", new BrowserSettings())) // Launch Chromium in off-screen mode { browser.Load(\"https://www.example.com\"); // Navigate to the test URL browser.Size = new Size(1920, 1080); // Set the browser size to 1920x1080 browser.ScreenshotAsync().ContinueWith(task => { var bitmap = task.Result; bitmap.Save(\"screenshot.png\", System.Drawing.Imaging.ImageFormat.Png); // Take a screenshot and save it as a PNG file }).Wait(); } Cef.Shutdown(); // Shutdown CEF Avec ca il faut ces using : using System; using System.Drawing; using System.IO; using CefSharp; using CefSharp.OffScreen; C'est assez prometteur m\u00eame si il faut encore beaucoup pour remplacer selenium. Ah bah lol en fait non on peut pas utiliser cette librarie pour faire tourner firefox... J'EN AI MARRE J'AVAIS CHERCHE PRECISEMENT UNE LIB QUI MARCHE AVEC FIREFOX Et phantomJS non plus ne fonctionne pas avec firefox... J'en ai marre. Donc je vais plut\u00f4t partir sur la librairie GeckoFX qui semble pouvoir contr\u00f4ler une instance de firefox. Mais j'avais justement pris un putain de projet C# et pas JS pour ne pas me taper ces probl\u00e8mes de librairies... Et si cette option ne fonctionne pas mon dernier espoir sera de directement int\u00e9ragir avec le geckodriver.exe et la ca risque de pas \u00eatre dr\u00f4le. JE NE COMPRENDS RIEN !!!!! Ca n'a aucun sens la doc est inexistante le seul lien qui pourrait amener sur une doc envoie sur la page principale de bitbucket. Tous les exemples de code que je trouve ne fonctionnent pas. Je n'arrive \u00e0 rien je commence \u00e0 devenir fou. Tout ce travail pour rien c'est pas possible. M\u00eame en essayant directement d'int\u00e9ragir avec le process geckodriver.exe je ne peux pas arriver \u00e0 mes fins. J'arrive \u00e0 lancer le service et tout, mais je n'arrive pas \u00e0 vraiment contr\u00f4ler ce qu'il se passe donc impossible de venir prendre des screenshot. Je ne sais tout simplement pas quoi faire ... Je suis bloqu\u00e9. Je me suis cass\u00e9 la t\u00eate \u00e0 faire un truc qui marchait bien avec selenium et tout. Mais maintenant plus rien ne fonctionne du jour au lendemain et il n'y a simplement aucune alternative. Je vais essayer de changer directement le projet Selenium_Clean mais bon ca va pas \u00eatre dr\u00f4le. Ok alors j'ai tout repris depuis le d\u00e9but et je crois que j'ai enfin une solution. Pour la trouver j'ai re-essay\u00e9 toutes les techniques que j'avais tent\u00e9 avant mais dans l'ordre et en les isolant \u00e0 chaque fois. Cela inclus : Tenter de changer la densit\u00e9 de pixels. En effet je me suis dit que comme la r\u00e9solution \u00e9tait plus basse le soucis \u00e9tait que le virtual screen avait simplement une DPI r\u00e9duite. profile.SetPreference(\"layout.css.devPixelsPerPx\", \"2.0\"); J'ai aussi tent\u00e9 de r\u00e9duire \u00e0 un seule le nombre de process de Firefox. J'ai pu lire sur internet que parfois cela pouvait influer sur les performances du renderer. profile.SetPreference(\"dom.ipc.processCount\", 1); Ensuite j'ai tent\u00e9 tout b\u00eatement de rajouter dans la liste des arguments la taille voulue de l'\u00e9cran. options.AddArgument(\"--window-size=1920,1080\"); Mais comme cela ne foncionnait pas, je me suis rabattu sur un script JS pour tenter de forcer la fen\u00eatre \u00e0 \u00eatre plus grande. js.ExecuteScript(\"window.resizeTo(1920, 1080);\"); Comme cela n'a pas march\u00e9 j'ai pu lire que cela pouvait \u00eatre la taille int\u00e9rieure qui devait \u00eatre chang\u00e9e js.ExecuteScript(\"window.innerWidth = 1920; window.innerHeight = 1080;\"); Encore une fois sans succ\u00e8s. J'ai ensuite tent\u00e9 d'utiliser trois autres versions du GeckoDriver, 0.27,0.26,0.25 et aucune ne m'aidait. Mais en fait la seule chose qui a chang\u00e9 quoi que ce soit \u00e9tait la technique suivante : Changer la window size en utilisant : options.AddArgument(\"--width=1920\"); options.AddArgument(\"--height=1200\"); Ca ne marchait pas car j'utilisais une autre methode pour resize en m\u00eame temps, qui elle ne marchait pas mais qui emp\u00eachait celle la de marcher. Ensuite le soucis que j'avais c'est que en mettant 1920-1080 je me retrouvais avec 1920-998 ou un truc du genre ce qui n'\u00e9tait pas normal alors je me disais que cette technique ne marchait pas non plus et je l'ai pass\u00e9e. Alors tout n'est pas encore gagn\u00e9, il faut que j'arrive \u00e0 impl\u00e9menter ca dans un plus gros projet et que la vid\u00e9o puisse \u00eatre prise seule. Demain je m'occupe de ca. Mardi 2 Mai 2023 Bon aujourd'hui je change le programme principal. Le soucis que j'ai c'est que en ajoutant ce syst\u00e8me de resize, maintenant la page fait 100x100 et est grise. Il doit y avoir une technique que j'ai oubli\u00e9 de retirer ou un comportement un peu bizarrre. Bon clairement je ne sais pas QUI DECIDE DE ME POURRIR LA VIE mais il est fort. J'ai t\u00e9l\u00e9charger EXACTEMENT les m\u00eames librairies que sur mon autre projet et j'utilise l'EXACT m\u00eame geckodriver.exe mais dans le projet principal impossible de lui faire chier une image m\u00eame avec l'EXACT m\u00eame code. POURQUOI VOUS ME FAITES CA????= La je ne comprend vraiment pas ce qui peut se passer pour que rien ne fonctionne alors que tout est pareil. JE VIENS DE TOUT VERIFIER TOUT EST PAREIL JE NE COMPRENDS PAS. Bon apr\u00e8s avoir supprim\u00e9 l'int\u00e9gralit\u00e9 de ma classe Emulator cela semble marcher un peu mieux. Je ne vais pas m'\u00e9tendre sur la castrophe niveau temps que cela repr\u00e9sente. Si au moins j'arrive \u00e0 faire fonctionner quelque chose je suis content. Maintenant j'ai un soucis un peu sp\u00e9cial. Depuis que j'ai chang\u00e9 la r\u00e9solution, il semble que le programme aie du mal \u00e0 cliquer sur l'icone de settings. En prenant des screenshots du moment ou l'erreur apparait, j'ai pu me rendre compte que en fait le stream est toujours en train de charger et c'est pour ca que on arrive pas \u00e0 trouver le bouton : \"ERROR 105\" \"ERROR 105\" Je pense que je n'ai le soucis que maintenant car le flux en 1080p se lance moins vite. Je vais essayer de voir si je peux detecter un \u00e9l\u00e9ment d'HTML qui correspond au loading comme ca je peux attendre qu'il disparaisse. Sinon je peux aussi juste essayer de trouver le bouton en boucle pendant une dixaine de secondes. Bon la j'essaie pendant genre plus de 50 secondes et ca ne marche toujours pas. Il semblerait que au final le probl\u00e8me vienne du GP d'azerbidjan. En effet, quand je teste un autre Grand Prix tout va bien. ET MERDE ! J'ai r\u00e9ussi \u00e0 avoir des images en 1080P mais d\u00e9s que je passe l'image en plein \u00e9cran c'est de nouveau du 1366X768 Avant de mettre en plein \u00e9cran: \"Before fullscreen\" Apr\u00e8s: \"After fullscreen\" On peut voir sur l'image que l'option 1080P est effectivement bien selectionn\u00e9e mais il doit y avoir un param\u00e8tre de Firefox qui s'occupe de la r\u00e9solution d'un player vid\u00e9o. Il va juste falloir trouver ce param\u00eatre... J'ai essay\u00e9 d'utiliser : Driver.Manage().Window.Size = new System.Drawing.Size(windowWidth, windowHeight); Sans succ\u00e8s. options.AddArgument(\"--start-maximized\"); Pareil Driver.Manage().Window.Maximize(); Toujours rien profile.SetPreference(\"full-screen-api.ignore-widgets\", true); Nada profile.SetPreference(\"media.hardware-video-decoding.enabled\", true); Toujours pas J'ai vraiment cru que j'avais trouv\u00e9 la solution en trouvant cette commande profile.SetPreference(\"full-screen-api.enabled\", true); Mais non toujours pas... Je commence \u00e0 perdre patience. C'EST BON. Apr\u00e8s litt\u00e9rallement 3h de debugging avec M.Bonvin (Que je remercie IMMENSEMENT) on a r\u00e9ussi \u00e0 trouver au fin fond d'un thread github que la valeur \u00e9tait hard cod\u00e9e dans les variables d'environnement et que donc quoi que je fasse je n'aurais pas pu le changer. En fait la seul moyen de tout r\u00e8gler a \u00e9t\u00e9 de changer les variables d'environnement de ma machine: MOZ_HEADLESS_WIDTH et MOZ_HEADLESS_HEIGHT . Et ce qu'il y a de bien c'est que maintenant je peux mettre de la 4K et cela permet de faire un meilleur upscaling. Recrutement Payerne Mai 2023 J'ai du faire mon recrutement \u00e0 Payerne Mercredi et Jeudi. Si vous \u00eates curieux je peux vous dire que comme il n'y avait presque plus de places cet \u00e9t\u00e9 je ferai Canonnier Lance mines. C'\u00e9tait assez frustrant d'avoir perdu deux jours de travail mais on va faire avec. Vendredi 5 Mai 2023 Bon malgr\u00e9s les courbatures il faut que je me mette au boulot un peu serieusement par ce que sinon ca va \u00eatre compliqu\u00e9 de rattraper mon retard. La derni\u00e8re fois si je me souviens bien j'avais r\u00e9ussi \u00e0 trouver un moyen de prendres des images en bonne r\u00e9solution. Il faut maintenant que je commence \u00e0 faire fonctionner la calibration et ce qui serait bien ca serait que je commence \u00e0 ajouter la partie OCR au projet. Il faut que je me d\u00e9p\u00eache car Lundi je dois m'occuper du Poster. OK j'ai compris le soucis que j'avais quand j'essayais de faire la calibration. J'avais mis l'image en ZOOM ce qui fait que si la hauteur n'\u00e9tait pas la bonne, l'image \u00e9tait recentr\u00e9e ce qui fait que cela faussait totalement les r\u00e9sultats. Quand on fait en sorte que l'image prenne toute la place, les coordonn\u00e9es sont prises correctement. Voici un exemple d'ou en est la partie calibration. \"Exemple settings UI\" Normalement il me suffit d'impl\u00e9menter les windows, et on devrait relativement facilement ajouter les pilotes. Et voila. J'ai pu impl\u00e9menter les windows et les pilotes. Et je peux aussi exporter des presets et les loader. Bon le loading est un peu beugg\u00e9 au niveau de l'affichage mais il semble qu'il fonctionne bien quand je save les images. Lundi je m'occupe du poster etc.. mais je pense que la suite va \u00eatre l'impl\u00e9mentation de l'OCR. Lundi 8 Mai 2023 Aujourd'hui c'est journ\u00e9e Poster. Je pense que je ne vais pas finir la journ\u00e9e content car les limitations sont un peu trop pr\u00e9sentes. J'ai fait une version que Garcia pourrait accepter, c'est \u00e0 dire en noir et blanc et avec un tout petit peu plus de d\u00e9tail. \"Poster V3\" Le truc c'est que en blanc je trouve que ca ne marche pas super. Et le concept d'avoir trois parties au projet qui se posent autour d'un circuit c'est peut-\u00eatre pas la meilleure id\u00e9e. Je me suis dit que la bonne id\u00e9e serait peut-\u00eatre de prendre un autre circuit pour qu'il y aie bien trois parties : \"Poster V4\" Clairement ce poster doit faire partie des pires. C'est pas clair et ca part dans tous les sens. Je vais essayer avec un autre layout de circuit. \"Poster V5\" Je me suis ensuite dit que le circuit n'\u00e9tait peut \u00eatre tout simplement pas une bonne id\u00e9e. J'ai donc essay\u00e9 de faire quelque chose de plus classique avec juste un peu de background pour qu'on puisse \u00e9viter le soucis de la page blanche derri\u00e8re : \"Poster V6\" Puis je me suis dit que finalement le circuit me manquait. Alors j'ai d\u00e9cid\u00e9 de combiner le background et le circuit ainsi que simplifier l\u00e9g\u00e8rement les diagrammes en retouchant un peu tout le reste on pouvait arriver \u00e0 quelque chose de sympatique : \"Poster V7\" Je ne suis pas content \u00e0 100% mais bon je pense que je vais m'en satisfaire. Pour donner une id\u00e9e de la gal\u00e8re que c'est de cr\u00e9er un poster, voici ce \u00e0 quoi ressemble mon espace de travail Figma : \"Bordel Figma\" Je ne suis pas un graphiste et ca se voit '^^. Je pense que comme il me reste un peu de temps aujourd'hui, je vais faire un peu de documentation de la partie r\u00e9cup\u00e8ration d'images. En effet, je pense que je n'aurai plus besoin de changer grand chose \u00e0 ce niveau. Mais je ne ferai pas la partie analyse fonctionnelle car l'interface n'est clairement pas termin\u00e9e. En fait j'avais oubli\u00e9 mais j'ai eu un rendez vous m\u00e9dical du coup je n'ai pas eu trop le temps de faire la doc que je voulais. Mais au moins je pense avoir finit mon travail sur le poster et le abstract en Anglais qui sont les deux gros livrables \u00e0 venir. Mardi 9 Mai 2023 Bon je viens de me rendre compte que apparemment on doit rendre l'abstract anglais, le Poster, ET LE PROJET. Je pense que mes deux jours \u00e0 l'arm\u00e9e m'ont fait perdre un peu la notion du temps car j'avais l'impression que l'evaluation interm\u00e9diaire 1 \u00e9tait il y a genre moins d'une semaine. Donc aujourd'hui je ne vais pas trop avancer sur le code et vraiment me focus sur la documentation de la r\u00e9cup\u00e8ration d'images. Je pense que je vais aussi ajouter la partie calibration \u00e0 la documentation. Je pense que c'est important que je prenne le temps maintenant car sinon le prof aura l'impression que ca n'a pas trop avanc\u00e9 depuis la derni\u00e8re fois. Et puis je pense que la partie calibration et r\u00e9cup\u00e8ration d'images ne va pas trop changer et la partie calibration encore moins. La partie anglaise je fais la revoir un peu mais je l'avais d\u00e9ja faite pendant les premiers jours alors ca devrait aller. Pour le rendu il nous \u00e9tait demand\u00e9 de fournir un fichier PDF avec tout dedans avec une table des mati\u00e8res notre code source etc... Pour ce faire j'ai du changer le mkdocs.yml et installer des packages. Voici les changements :: site_name: Documentation Track Trends site_author: Rohmer Maxime copyright: \u00a9CFPTI Tech2 theme: name: material palette: # Palette toggle for light mode - media: \"(prefers-color-scheme: light)\" scheme: default toggle: icon: material/brightness-7 name: Switch to dark mode # Palette toggle for dark mode - media: \"(prefers-color-scheme: dark)\" scheme: slate toggle: icon: material/brightness-4 name: Switch to light mode markdown_extensions: - attr_list - md_in_html - pymdownx.highlight plugins: - glightbox - search - img2fig - with-pdf: cover_subtitle: Vroum Vroum enabled_if_env: ENABLE_PDF_EXPORT - annexes-integration: annexes: # Required (at least 1) - ConfigurationTool.cs: Code/ConfigurationTool.cs # An path to an annex with its title - DriverGapToLeaderWindow.cs: Code/DriverGapToLeaderWindow.cs # An path to an annex with its title - DriverPositionWindow.cs: Code/DriverPositionWindow.cs # An path to an annex with its title - F1TVEmulator.cs: Code/F1TVEmulator.cs # An path to an annex with its title - Program.cs: Code/Program.cs # An path to an annex with its title - Window.cs: Code/Window.cs # An path to an annex with its title - DriverData.cs: Code/DriverData.cs # An path to an annex with its title - DriverLapTimeWindow.cs: Code/DriverLapTimeWindow.cs # An path to an annex with its title - DriverSectorWindow.cs: Code/DriverSectorWindow.cs # An path to an annex with its title - Form1.cs: Code/Form1.cs # An path to an annex with its title - Reader.cs: Code/Reader.cs # An path to an annex with its title - Zone.cs: Code/Zone.cs # An path to an annex with its title - DriverDrsWindow.cs: Code/DriverDrsWindow.cs # An path to an annex with its title - DriverNameWindow.cs: Code/DriverNameWindow.cs # An path to an annex with its title - DriverTyresWindow.cs: Code/DriverTyresWindow.cs # An path to an annex with its title - OcrImage.cs: Code/OcrImage.cs # An path to an annex with its title - Settings.cs: Code/Settings.cs # An path to an annex with its title - recoverCookiesCSV.py: Code/recoverCookiesCSV.py # An path to an annex with its title Je remercie Monsieur Briard le sultan officiel de Mkdocs de la classe de m'avoir aid\u00e9 pour cette partie et avoir cr\u00e9\u00e9 un plugin qui me permet de mettre mon code source directement dans le pdf. Bon au final j'ai quand m\u00eame chang\u00e9 mon poster \"Poster V8\" Mais je suis trop attach\u00e9 \u00e0 l'ancien concept alors je vais plut\u00f4t utiliser ca : \"Poster V9\"","title":"Journal de bord"},{"location":"jdb.html#journal-de-bord","text":"","title":"Journal de bord"},{"location":"jdb.html#mercredi-29-mars-2023","text":"Premier jour du travail de dipl\u00f4me. Nous avons eu un briefing de mr Garcia et nous avons pu commencer \u00e0 pr\u00e9parer le travail. Nous avons eu les diff\u00e9rents fichiers nescessaires \u00e0 la bonne r\u00e9alisation du projet et je me suis mis \u00e0 faire les fichiers nescessaires La premi\u00e8re chose a \u00e9t\u00e9 de faire ce mkdocs dans lequel j'ai mis un fichier yml plut\u00f4t standart qui risque de changer au fur et \u00e0 mesure. Voici le premier yml : site_name: Documentation Diplome theme: name: material palette: # Palette toggle for light mode - media: \"(prefers-color-scheme: light)\" scheme: default toggle: icon: material/brightness-7 name: Switch to dark mode # Palette toggle for dark mode - media: \"(prefers-color-scheme: dark)\" scheme: slate toggle: icon: material/brightness-4 name: Switch to light mode markdown_extensions: - attr_list - md_in_html plugins: - glightbox - with-pdf Voici un example de \u00e0 quoi ca ressemble en forme de site \"Exemple mkdocs\" Ensuite il m'a fallu faire une version plus \u00e0 jour de mon cahier des charges car je n'y avait pas touch\u00e9 depuis novembre. J'ai envoy\u00e9 un mail \u00e0 mes enseignants pour qu'ils puissent y jeter un oeuil pour \u00eatre s\u00fbr que je n'ai rien chang\u00e9 qui les d\u00e9rangent. Monsieur Jayr m'a demad\u00e9 \u00e0 l'occasion de lui faire un planning type Gantt alors je me suis mis \u00e0 la t\u00e2che. J'ai fait un planning pr\u00e9visionnel et une l\u00e9gende les deux sont dispo dans le dossier planning de ce repertoire. Ensuite je me suis mis \u00e0 tout mettre sur git. A commencer par ce repertoire Et c'est deja la fin de la journ\u00e9e ! Demain j'avance un peu sur la doc avec ce que je peux d\u00e9ja remplir et je finis de pr\u00e9parer ce dont j'ai besoin pour commencer \u00e0 coder.","title":"Mercredi 29 Mars 2023"},{"location":"jdb.html#jeudi-30-mars-2023","text":"Aujourd'hui selon le planning je dois me charger des dernirers pr\u00e9paratifs pour commencer correctement. J'ai fait expr\u00e8s de prenre du temps pour ca au d\u00e9but pour ne pas me cr\u00e9er de soucis plus loin pendant le travail. Je vais envoyer par mail le planning que j'ai fait \u00e0 mes suiveurs. Ensuite je vais m'attaquer au squelette de la docmentation. Je vais essayer de remplir tout ce que je peux remplir dans un premier temps car cela tout ca de fait pour plus tard quitte \u00e0 modifier quelques aspects au fur et \u00e0 mesure. J'ai aussi d\u00e9sactiv\u00e9 mkdocs with pdf par ce que les r\u00e9sultats ne sont vraiment pas ceux que j'attends et cela ralentis \u00e9norm\u00e9ment le d\u00e9ploiment. J'ai aussi rassembl\u00e9 mes croquis pour le poster : \"Croquis Poster 1\" \"Croquis Poster 2\" On peut voir que dans un premier temps j'ai tent\u00e9 de faire un poster un peu plus stylis\u00e9 et marketing. Cependant apr\u00e8s avoir discut\u00e9 avec Mr Garcia et diff\u00e9rents profs dont un de l'HEPIA et ils m'ont indiqu\u00e9 que ce qui \u00e9tait attendu \u00e9tait moins du marketing qu'un diagramme de fonctionnement. On peut voir sur les derniers posters que le cot\u00e9 technique ressort de plus en plus. Le but sera de faire une version encore plus technique ou on peut voir les diff\u00e9rents fonctionnements de l'application avec les technologies utilis\u00e9es. Le d\u00e9fi cela va \u00eatre de faire un joli poster qui soit en m\u00eame temps vendeur et en m\u00eame temps rempli techniquement. Oh et j'ai eu un probl\u00e8me ou mon calvier et ma souris ne voulaient d'un coup plus fonctionner. Dans mon cas c'\u00e9tait un probl\u00e8me de power management des ports. J'ai eu le soucis sur mon pc fixe \u00e0 la maison et sur mon pc portable \u00e9galement. En gros de ce que j'ai compris le soucis c'est que le pc croit que un port est trop solicit\u00e9 niveau puissance et du coup d\u00e9cide de couper l'alimentation du port USB. J'ai pu r\u00e8gler le soucis en allant dans le device manager sous universal bus controller sous power management et en d\u00e9cochant la case qui indique que windows peut d\u00e9sactiver ce port. Je ne conseille pas ce fix si vous avez des composants de mauvaise qualit\u00e9 car cela pourrait \u00eatre une vraie alerte cependant le fait que mes composants sont plut\u00f4t haut de gamme et le fait que mon clavier et ma souris le fassent en m\u00eame temps et que ils fonctionnaient tr\u00e8s bien depuis plus de 4 ans me font penser que c'est juste une nouvelle mise a jour de windows qui est p\u00e9nible. Demain je vais pouvoir commencer \u00e0 coder pour de bon.","title":"Jeudi 30 Mars 2023"},{"location":"jdb.html#vendredi-31032023","text":"Aujourd'hui on s'occupe de la PT2 qui est la programmation de la r\u00e9cup\u00e8ration des informations des images. Je vais tester IronOcr Source : https://www.c-sharpcorner.com/article/ocr-using-tesseract-in-C-Sharp/ Doc : https://ironsoftware.com/csharp/ocr/docs/ Examples : https://ironsoftware.com/csharp/ocr/examples/simple-csharp-ocr-tesseract/ Avant d'utiliser la librairie je me demande si je dois utiliser un peu de post processing pour aider \u00e0 la reconnaissance. Je peux soit utiliser l'image crop\u00e9e directement : \"Image non trait\u00e9e\" Soit avec un filtre pour passer en noir et blanc laxiste \"Image trait\u00e9e laxiste\" Soit avec un filtre pour passer en noir et blanc stricte \"Image trait\u00e9e stricte\" Il va falloir faire des tests avec tous les noms et les chiffres pour trouver le plus efficace. Bon malheureusment Iron OCR semblait \u00eatre une bonne alternative mais c'est une librairie priv\u00e9e qui demande une license pour \u00eatre utilis\u00e9e. Il va falloir trouver autre chose. En utilisant la librairie \"Tesseract\" qui existe on peut faire de la reconnaissance de texte avec un code plut\u00f4t simple : TesseractEngine engine = new TesseractEngine(tessDataFolder.FullName,\"eng\", EngineMode.Default); var tessImage = Pix.LoadFromMemory(ImageToByte(sample)); Page page = engine.Process(tessImage); string text = page.GetText(); Voici la methode ImageToByte : https://stackoverflow.com/questions/7350679/convert-a-bitmap-into-a-byte-array public static byte[] ImageToByte(Image img) { using (var stream = new MemoryStream()) { img.Save(stream, System.Drawing.Imaging.ImageFormat.Png); return stream.ToArray(); } } Voici le code pour traiter plusieurs textes sur une seule image : Page page = engine.Process(tessImage); // Get the iterator for the page layout using (var iter = page.GetIterator()) { // Loop over the elements of the page layout iter.Begin(); do { // Declare a Rect variable to hold the bounding box Rect boundingBox; // Get the bounding box for the current element if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox)) { g.DrawRectangle(Pens.Red,new Rectangle(boundingBox.X1,boundingBox.Y1,boundingBox.Width,boundingBox.Height)); } // Get the text for the current element var text = iter.GetText(PageIteratorLevel.Word); tbxResult.Text += text.ToUpper() + Environment.NewLine; } while (iter.Next(PageIteratorLevel.Word)); } Etonnament, avec plus de texte, des noms qui \u00e9taient autrefois mal reconnus sont parfaitement interpr\u00eat\u00e9s. Par exemple voici un exemple de reconnaisance de texte sur tous les pilotes : \"Screenshot de reconnaisance d'image complete\" On voit que le nom Leclerc est mal reconnu. Mais voici ce que cela donne quand on prend une image qui ne contient que le nom Leclerc : \"Screenshot de reconnaissance d'image crop\u00e9e\" On voit ici que le nom Leclerc est tr\u00e8s bien reconnu. Dans le premier exemple on peut voir que Tsunoda est reconnu comme \"Reticin\" ce qui n'est pas exactement pareil (lol) Et quand on isole le nom Tsunoda dans une image seule : \"Screenshot de reconnaissance de Tsunoda\" Il le lit \"RETLELYY\" ce qui n'est toujours pas exactement ca... Une meilleure r\u00e9solution pourrait peut-\u00eatre r\u00e9soudre le probl\u00e8me en partie. Jusqu'ici les images \u00e9taient en presque 720P ce qui donne ceci : \"Tsunoda en 720P\" Et j'ai lanc\u00e9 une r\u00e9cup\u00e8ration d'images en 1080p pour r\u00e9cup\u00e8rer ceci : \"Tsunoda en 1080P\" On peut voir une certaine diff\u00e9rence tout de m\u00eame. Et quand on lance la reconnaissance : \"Reconnaissance Tsunoda en 1080P\" \"Tsunoda n'est plus \u00e9crit \"RETLELYY\" mais \"TSUNDDA\" ce qui n'est pas parfait mais qui est d\u00e9ja beaucoup mieux. J'ai essay\u00e9 de mettre l'engine de Tesseract en mode \"JPN\" comme Tsunoda est un nom japonais mais sans succ\u00e8s j'ai le m\u00eame r\u00e9sultat. Comme la r\u00e9solution est meilleure je me suis dit que peut \u00eatre le filtre de passage en noir et blanc pourrait aider. J'ai \u00e9crit cette petite methode pour convertir l'image en noir et blanc : private static Bitmap ConvertToBlackAndWhite(Bitmap inputBmp) { const int BLACK_TO_WHITE_TRESHOLD = 200; Bitmap result = new Bitmap(inputBmp.Width, inputBmp.Height); for (int y = 0; y < inputBmp.Height; y++) { for (int x = 0; x < inputBmp.Width; x++) { Color pixelColor = inputBmp.GetPixel(x,y); if (pixelColor.R <= BLACK_TO_WHITE_TRESHOLD && pixelColor.G <= BLACK_TO_WHITE_TRESHOLD && pixelColor.B <= BLACK_TO_WHITE_TRESHOLD) { pixelColor = Color.FromArgb(0,0,0); } else { pixelColor = Color.FromArgb(255,255,255); } result.SetPixel(x,y,pixelColor); } } return result; } Rien de bien dingue mais cela fonctionne et je peux jouer avec le BLACK_AND_WHITE_TRESHOLD pour changer son comportement. J'ai dabord test\u00e9 avec un treshold de 100 et le programme a r\u00e9ussi \u00e0 me sortir Tsunoda en deux mots ce qui \u00e9tait d\u00e9ja tr\u00e8s encourageant. Et apr\u00e8s avoir augment\u00e9 le Treshold... Tada : \"Tsunoda 1080P avec filtre\" Le programme arrive bien \u00e0 reconnaitre TSUNODA. Je pense que cette tactique ne fonctionnait pas avant car la resolution \u00e9tait trop faible et l'aliasing se m\u00ealait trop avec le texte pour \u00eatre utilisable. Cependant cette technique ne fonctionne pas sur tous les noms. Par example avec Leclerc : \"Leclerc 1080P avec filtre\" On r\u00e9cup\u00e8re \"Leeler'c\" ce qui n'est pas bon du tout. Mais en modulant le Treshold (ici \u00e0 150) On peut de nouveau voir Leclerc \u00eatre reconnu correctement \"Leclerc 1080P avec filtre 2\" Je pense que pour avoir de bons r\u00e9sultats il va falloir faire un algo qui : D\u00e9coupe l'image en autant de plus petites images pour avoir un mot par image. Teste voir si avec l'image originale un nom correspond \u00e0 la liste de pilotes existant. Si cela ne marche pas, on applique le filtre en modulant le Treshold. Dans le cas ou on aurait pas un match parfait on fait un algo qui cherche le nom le plus proche qui existe dans la liste de noms donn\u00e9s. Seulement voila, il n'y a pas que des lettres que l'on veut r\u00e9cup\u00e8rer. On veut surtout pouvoir r\u00e9cup\u00e8rer les chiffres. Pour les chiffres on va avoir des soucis \u00e9galement... Si on essaie directement la m\u00eame technique sans filtre on a des r\u00e9sultats comme celui ci : \"Tentative de reconnaisance du timing\" La virgule a tendeance \u00e0 se barrer ce qui est particuli\u00e8rement probl\u00e9matique. Cependant comme les chiffres ont beaucoup moins de possibilit\u00e9es que les lettres et qu'il n'y a pas de probl\u00e8me de langue on devrait pouvoir travailler \u00e0 faire des r\u00e8glage que l'on pourra ensuite utiliser. Avec un Treshold de 165 on arrive presque \u00e0 quelque chose d'int\u00e9ressant : \"Tentative 2 de reconnaissance du timing\" Le + n'est clairement pas compris mais ca n'est pas emb\u00eatant car c'est souvent redondant. On arrive cependant \u00e0 isoler 3 et 259. M\u00eame si la virgule n'est pas comprise cela veut dire qu'il est tout de m\u00eame possible de discriminer les secondes des milisecondes. Maintenant avec un temps au tour : \"Reconnaissance du timing au tour\" On arrive sans rien changer aux param\u00eatres \u00e0 isoler minutes secondes et milisecondes. Il semble que la reconnaissance de chiffre soit bien plus efficace que la reconnaissance de lettres. Il va falloir faire un test \u00e0 plus grande \u00e9chelle avec plus d'image pour se rendre compte de la precision. Demain ce qui serait bien cela serait que je fasse un jeu d'images avec des valeurs connues et que je fasse une batterie de tests pour voir \u00e0 quel point je peux faire confiance \u00e0 la reconnaissance des chiffres. Automatiser un syst\u00e8me de test de la sorte me sera tr\u00e8s utile dans le futur pour v\u00e9rifier la non regression de ma reconnaissance de texte quand je tenterai d'y faire des changements. Je suis toujours curieux cependant de voir comment le programme se d\u00e9brouille avec les nombres de tours qui se trouvent dans les icones de pneus.","title":"Vendredi 31/03/2023"},{"location":"jdb.html#lundi-3-avril","text":"Aujourd'hui on va faire un programme qui permet de cr\u00e9er un dataset qui permette de tester le programme de reconnaissance. Je pense que le meilleur moyen de faire serait un programme qui cr\u00e9e le dataset et qui ensuite peut tester diff\u00e9rentes methodes de reconnaissance. Par la m\u00eame occasion je peux d\u00e9velopper la technologie qui va permettre de d\u00e9couper une image en 20 lignes ce qui me servira ensuite pour la reconnaissance. Je me rend compte que pour faire un programme de tests je dois d\u00e9ja avoir une id\u00e9e de la structure de mon programme. Pour le moment je r\u00e9flechis \u00e0 un syst\u00e8me de \"Zones\" et de \"Windows\". L'id\u00e9e serait que une Zone est juste une sous partie d'image qui peut encore \u00eatre d\u00e9compos\u00e9 tandis que chaque Window contient une ou plusieurs informations \u00e0 r\u00e9cup\u00e8rer. J'ai essay\u00e9 de d\u00e9couper l'image pour que cela soit plus clair : \"Main zone\" Ici on peut voir que l'image est d\u00e9coup\u00e9e en plusieurs grandes zones. Dans un premier temps on ne s'occupe que de la premi\u00e8re. Ensuite : \"Driver zone #1\" On peut voir la que cette Main zone serait elle m\u00eame d\u00e9compos\u00e9e en plusieurs plus petites zones. Et ensuite chacunes de ces petites zones : \"Driver windows\" Sera d\u00e9compos\u00e9e en plusieurs windows qui elles sont des zones qui contiennent de l'information. En gros on aurait trois types de zone : Les zones qui contiennent d'autres zones Les zones qui contiennent des Windows Les Windows Cependant en y r\u00e9flechissant on pourrait tout \u00e0 fait avoir seulement des zones et des windows en faisant en sorte que les windows peuvent avoir une liste de windows et une liste de zones. Une zone serait compos\u00e9e de : Une image de d\u00e9part Un rectangle qui la positionne sur cette derni\u00e8re Une liste de zones (potentiellement vide) Une liste de windows (potentiellement vide) Une methode qui permet de r\u00e9cup\u00e8rer une image de la zone Une methode qui permet de lancer la reconnaissance sur chaque window Une window serait compos\u00e9e de : Une image de d\u00e9part (cela peut \u00eatre l'image crop\u00e9e de la zone parente peu importe) Un rectangle qui la positionne sur cette derni\u00e8re Une methode qui permet de r\u00e9cup\u00e9rer un image de la window Une methode qui permet de lancer la reconnaisance sur l'image (Chaque type de zone doit l'impl\u00e9menter) Dans chaque window on peut imaginer que la methode qui fait la reconnaissance au lieu de retourner un objet qui peut contenir nimporte quel type d'information peut envoyer ce qu'elle vient de r\u00e9cup\u00e8rer dans une base de donn\u00e9e ou un objet. Par exemple une Zone de pilote pourrait tr\u00e8s bien contenir un objet pilote et le donner \u00e0 ses windows qui rempliraient ce m\u00eame objet. C'est une reflexion plus stockage que OCR mais c'est int\u00e9ressant pour savoir ce que fait une window des donn\u00e9es qu'elle r\u00e9cup\u00e8re. Dans un premier temps je pense que les windows vont simplement \u00e9crire dans un fichier ce qu'elles trouvent chacunes dans le format qu'elles veulent. Pour comprendre pourquoi je me prend la t\u00eate il faut savoir que chaque window peut avoir acc\u00e8s \u00e0 pleins d'informations diff\u00e9rentes. On pourrait dire qu'elles retournent toutes une string sauf que si ca marche pour un temps au tour ou pour un nom de pilote, cela ne marche pas forc\u00e9ment pour un type de pneu ou un DRS ouver. Comme chaque window a plusieurs types de data elle devra elle m\u00eame se charger de comment la traiter ET de la stocker. Voila un diagramme qui r\u00e9sume comment je vois l'impl\u00e9mentation dans un premier temps : \"Diagramme d'explications\" Voici comment se pr\u00e9sente le squellette d'une Zone : public class Zone { private Bitmap FullImage; private List Zones; private List Windows; private Rectangle _bounds; public Rectangle Bounds { get => _bounds; private set => _bounds = value; } public Bitmap ZoneImage { get { Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(FullImage, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); return sample; } } public Zone(Image fullImage, Rectangle bounds) { FullImage = (Bitmap)fullImage; Init(bounds); } public Zone(Bitmap fullImage, Rectangle bounds) { FullImage = fullImage; Init(bounds); } private void Init(Rectangle bounds) { Bounds = bounds; Zones = new List(); Windows = new List(); } public void AddZone(Rectangle bounds) { if(Fits(bounds)) Zones.Add(new Zone(ZoneImage,bounds)); } public void AddWindow(Rectangle bounds) { if (Fits(bounds)) Windows.Add(new Window(ZoneImage,bounds)); } private bool Fits(Rectangle inputRectangle) { if (inputRectangle.X + inputRectangle.Width > Bounds.Width || inputRectangle.Y + inputRectangle.Height > Bounds.Height || inputRectangle.X < 0 || inputRectangle.Y < 0) { return false; } else { return true; } } } Le but est ensuite de cr\u00e9er diff\u00e9rent types de Zones. Par exemple la MainZone devra d\u00e9couper son contenu en 20 parties \u00e9gales pour tenter de chopper les 20 pilotes. Il serait cool de trouver un moyen de calibrer automatiquement. C'est peut-\u00eatre possible de calibrer avec de la reconnaissance de texte, on peut essayer de lancer la reconnaissance et voir ou on trouve du texte avec un peu de chance cela pourrait donner les positions et avec ca on peut peut-\u00eatre determiner des lignes. Et voici le squelette d'une window g\u00e9n\u00e9rique using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace OCR_tester { public class Window { private Bitmap FullImage; private Rectangle _bounds; public Rectangle Bounds { get => _bounds; private set => _bounds = value; } public Bitmap WindowImage { get { Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(FullImage, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); return sample; } } public Window(Bitmap fullImage, Rectangle bounds) { FullImage = fullImage; Bounds = bounds; } public virtual void RecoverInformations() { //Each Window type will have to implement its own way to recover the informations stored in the Window Image } } } Chaque Window pourra ainsi elle m\u00eame impl\u00e9menter la r\u00e9cup\u00e8ration d'informations. La facon de les retourner/stocker est encore un peu floue. Par exemple pour un temps au tour on peut imaginer que il fait une petite v\u00e9rification dans l'objet pilote et dans le tableau des tours si il n'y a pas deja une valeur et si il n'y en a pas une alors il peut l'ajouter. Maintenant je vais essayer de cr\u00e9er une Main window qui se calibre toute seule. Alors apr\u00e8s avoir bien gal\u00e8r\u00e9 avec l'interface pour permettre au user de cliquer sur la form pour voir les zones qu'il cr\u00e9e, j'ai pu cr\u00e9er un zone qui fait les dimensions de MainZone et j'ai pu lancer la reconnaissance sur l'image et voir ou il trouve du texte : \"MainZone avec carr\u00e9s de texte\" Maintenant il faut que je nettoie la liste de rectangle pour exclure ceux qui sont trop grands pour \u00eatre sur une seule ligne, ceux qui indiquent le nombre de tour en haut et ceux qui n'ont pas d'int\u00e9r\u00eats. On pourra ensuite isoler les lignes et cr\u00e9er une liste d'images. Pour ce qui est de la ligne qui contient les \"Gap interval last lap\" et des chiffres sur les tours pour les pneus etc je vais juste demander \u00e0 l'utilisateur de ne pas les prendre dans la screenshot. Comme ils contiennent des mots qui peuvent \u00eatre utilis\u00e9s plus loin dans les data je ne peux pas les blacklister et faire un syst\u00e8me qui s'occupe de les enlever si ils existent selon le position y me prendrait trop de temps pour rien. Apr\u00e8s avoir filtr\u00e9 un peu les resultats et enlev\u00e9 les zones beaucoup trop grandes, on se retrouve d\u00e9ja plus qu'avec ca : \"MainZone avec de meilleurs carr\u00e9s\" Comme on peut le voir, du c\u00f4t\u00e9 gauche de l'image on a beaucoup de choses reconnues mais avec beaucoup de tailles diff\u00e9rentes ce qui n'est pas id\u00e9al. Alors j'ajoute un filtre qui permet de ne selectionner que les data sur la droite. \"MainZone avec de meilleurs carr\u00e9s\" Maintenant il devrait \u00eatre possible de faire un algorythme qui ne prend que un seul carr\u00e9 par ligne. \"MainZone avec un seul carr\u00e9 par ligne\" Maintenant que on sait ou se trouve chaque ligne on peut faire un petit traitement et d\u00e9couper l'image en plusieurs windows. Et voila : \"Mainzone auto calibr\u00e9e\" Maintenant le programme peut cr\u00e9er des zones pour chaque pilote \"Images pilotes\" \"Zone d'un pilote\" Maintenant il faut que j'impl\u00e9mente un syst\u00e8me un peu similaire pour cr\u00e9er des windows. Voici la methode que j'ai cr\u00e9\u00e9 pour l'autocalibration : public void AutoCalibrate() { List detectedText = new List(); Zones = new List(); TesseractEngine engine = new TesseractEngine(Window.tessDataFolder.FullName, \"eng\", EngineMode.Default); Image image = ZoneImage; var tessImage = Pix.LoadFromMemory(Window.ImageToByte(image)); Page page = engine.Process(tessImage); using (var iter = page.GetIterator()) { iter.Begin(); do { Rect boundingBox; if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox)) { //var text = iter.GetText(PageIteratorLevel.Word).ToUpper(); //We remove all the rectangles that are definitely too big if (boundingBox.Height < image.Height / NUMBER_OF_DRIVERS) { //Now we add a filter to only get the boxes in the right because they are much more reliable in size if (boundingBox.X1 > image.Width / 2) { //Now we check if an other square box has been found roughly in the same y axis bool match = false; //The tolerance is roughly half the size that a window will be int tolerance = (image.Height / NUMBER_OF_DRIVERS) / 2; foreach (Rectangle rect in detectedText) { if (rect.Y > boundingBox.Y1 - tolerance && rect.Y < boundingBox.Y1 + tolerance) { //There already is a rectangle in this line match = true; } } //if nothing matched we can add it if(!match) detectedText.Add(new Rectangle(boundingBox.X1, boundingBox.Y1, boundingBox.Width, boundingBox.Height)); } } } } while (iter.Next(PageIteratorLevel.Word)); } foreach (Rectangle Rectangle in detectedText) { Rectangle windowRectangle; Size windowSize = new Size(image.Width, image.Height / NUMBER_OF_DRIVERS); Point windowLocation = new Point(0, (Rectangle.Y + Rectangle.Height / 2) - windowSize.Height / 2); windowRectangle = new Rectangle(windowLocation, windowSize); Zones.Add(new Zone(ZoneImage, windowRectangle)); } } Ca peut paraitre pas \u00e9norme comme code mais pour tout mettre en place ca demande quand m\u00eame pas mal de reflexion. J'ai du clean un peu le code que j'avais fait pour permettre la selection de zones et ajouter la possibilit\u00e9 d'ajouter des windows sur une zone. J'ai juste quelques difficult\u00e9es \u00e0 les ajouter correctement, j'ai un offset tout pourri qui se met tout le temps \"Sainz coup\u00e9\" \"Perez coup\u00e9\" Cela doit \u00eatre un soucis lors de la detection de clic qui met un offset en trop. C'est vraiment p\u00e9nible en tout cas. Certes c'est moins fun de devoir manuellement indiquer ou sont les windows sur une ligne de pilote, mais je ne vois vraiment pas comment faire cela automatiquement. Le but c'est de faire une configuration qui puisse \u00eatre sauvegard\u00e9e comme ca pas besoin d'\u00e0 chaques fois le refaire. C'est bon ! J'avais juste oubli\u00e9 de changer le calcul d'offset entre le code de la zone et de la window. Note pour plus tard, il serait peut-\u00eatre judicieux de faire quelque chose pour la vue, les windows et les Zones ont le m\u00eame exact comportement pour la vue ce qui fait dupliquer du code. Mais au moins maintenant ca fonctionne : \"Ocr tester screenshot\" Et le programme va directement cr\u00e9er un dossier par pilote avec toutes les images de chaque Data le concernant : \"Dossier Perez\" ; Et c'est tout pour aujourd'hui je pense. Ce qui serait cool demain c'est que je puisse stocker d'une mani\u00e8re ou d'une autre ces fichiers de calibration et que je puisse les transf\u00e8rer vers le programme qui va s'occuper de d\u00e9coder et commencer gentillement \u00e0 d\u00e9coder les diff\u00e9rents types de data. Note pour quand je ferai les tests. Je pense que la meilleure id\u00e9e serait que je prenne pleins de photos du style et que je les mette dans un fichier CSV ou JSON avec leur contenu. Et ensuite je le fais passer en tests pour calculer la prescision de mon algo de d\u00e9codage. Pour le moment on est plut\u00f4t dans les clouts niveau planning.","title":"Lundi 3 Avril"},{"location":"jdb.html#mardi-4-avril","text":"Aujourd'hui je suis scens\u00e9 plut\u00f4t bosser sur l'interpretation des donn\u00e9es, mais une id\u00e9e m'a taraud\u00e9 l'esprit toute la nuit. Est-ce que je ne pourrais pas quand m\u00eame essayer de d\u00e9composer la zone de pilote directment comme pour la Main zone. Pour ce faire j'ai tent\u00e9 de faire comme pour la main zone c'est \u00e0 dire lancer la reconnaissance pour savoir ou \u00e9taient tous les champs de donn\u00e9es mais malheureusement je ne pense pas que cela va \u00eatre possible. En effet non seulement ici les champs sont de tailles tr\u00e8s vari\u00e9es, mais en plus la reconnaissance n'arrive pas \u00e0 en r\u00e9cup\u00e8rer le m\u00eame nombre sur chaque ligne ce qui risque d'\u00eatre complexe \u00e0 utiliser ensuite. La preuve : \"Tentative d'auto calibration\" ; Cependant tout n'est pas perdu ! Il y a peut-\u00eatre un moyen qui serait mieux en tous points. Le soucis avec ce type de reconnaissance c'est qu'on utilise beaucoup de ressources inutiles. On peut peut-\u00eatre hard coder la valeur des diviseurs et les utiliser pour cr\u00e9er des zones. Ok alors visiblement c'est un probl\u00e8me car il semble y avoir d'autres pixels de cette couleur dans l'image (Qui l'aurait cru lol) \"Tentative 2\" J'a tent\u00e9 de r\u00e9duire la tol\u00e9rance mais le soucis c'est que c'est soit trop soit pas assez Derni\u00e8re tentative, j'ai essay\u00e9 de prendre plusieurs pixels en hauteur pour chaque incr\u00e9ment de X et en faire la moyenne, et m\u00eame comme ca, impossible de trouver de mani\u00e8re efficace les zones. Je pense que je vais donc revert tous mes changements pour revenir \u00e0 la version ou on les choisissait manuellement. Pas mal de temps perdu mais bon c'est comme ca ca arrive Bon j'ai fait un revert mais j'ai ajout\u00e9 une feature importante. Les zones font la largeur indiqu\u00e9e par l'utilisateur mais elles font la hauteur max comme ca toutes les window font la m\u00eame hauteur et ca permet \u00e0 l'utilisateur de ne pas forc\u00e9ment \u00eatre ultra pr\u00e9cis dans sa selection. Ce qui nous donne : \"Resultat final\" Maintenant je dirais que les deux prochaines choses \u00e0 faire seraient de stocker ces zones dans un fichier JSON ou autre pour que la calibration puisse \u00eatre envoy\u00e9e directement dans le logiciel de reconnaissance et ensuite de faire une calibration sur des images qui font la taille qu'on aura pendant les Grands Prix. Pour le moment elles sont au format 16:10 qui est le format d'\u00e9crant de mon laptop. Pour le stockage j'imagine un fichier qui donne des indications assez simples qui permettent de reconstruire le total des zones quand il est import\u00e9 plutot que d'\u00e9crire les coordonn\u00e9es en dur pour chacunes. Chaque Grande zone va impl\u00e9menter une methode qui s'occupe de mettre tous ses enfants dans un fichier. { \"MainZone\":{ \"x\": 10, \"y\": 20, \"width\": 1450, \"height\": 1340, \"DriverZone\":{ \"x\": 0, \"y\": 23, \"height\": 25, \"Windows\":[ { \"DriverPositionWindow\":{ \"x\": 0, \"y\": 0, \"width\": 35 } }, { \"DriverPositionChangesWindow\":{ \"x\": 0, \"y\": 0, \"width\":45 } } ] } } } C'est le r\u00e9sultat auquel j'aimerais arriver. Mais pour y arriver il faut encore que je cr\u00e9e les diff\u00e9rents types de window. Cela veut dire que je dois d\u00e9cider quelles informations je vais r\u00e9cup\u00e8rer de la page. Par exemple je vais conserver la position du pilote mais au final les changements de positions sont difficiles \u00e0 lire et sont redondants. Si je garde un historique des positions des pilotes je peux calculer moi m\u00eame les changements. Pareil pour gap avec la voiture devant. Je pense que je vais juste garder l'information des \u00e9carts absolus et ensuite je pourrai toujours calculer la diff\u00e9rence entre les pilotes. Ca peut para\u00eetre b\u00eate car cela rajoute du calcul mais en r\u00e9alit\u00e9 le calcul de l'OCR est extr\u00eamement gourmand alors il faut que j'\u00e9vite le plus possible d'y faire recours. Il est bien plus rapide de calculer les \u00e9carts que d'essayer de reconnaitre le texte et le convertir en chiffre. J'ai visiblement ajout\u00e9 un bug dans mon code. Maintenant tous les pilotes ont la m\u00eame image quand on les selectionne. Mais visiblement ca n'\u00e9tait pas le cas avant car j'avais pu prendres des images de chaque pilote. J'ai pass\u00e9 3 minutes \u00e0 fixer un bug stupide j'ai un peu envie de br\u00fbler ma place de travail... Mais bon au moins maintenant cela fonctionne ! Toutes les images sont r\u00e9cup\u00e8r\u00e9es et ont un format correct avec le bon nom : \"Verstappen folder\" Avec un peu de code tr\u00e8s moche j'ai pu cr\u00e9er un fichier JSON qui contient les diff\u00e9rentes infos. Cependant en exportant TOUT on se retrouve avec un fichier de 1200 lignes ce qui n'est pas optimal. Mais quand on regarde, il devrait \u00eatre possible de faire un fichier qui ne contient que les infos d'un seul pilote car ensuite il y a simplement un offset \u00e0 appliquer sur la zone et les windows. Je vais donc pouvoir commencer enfin le logiciel de d\u00e9codage qui prend en entr\u00e9e un fichier JSON comme celui ci qui a \u00e9t\u00e9 g\u00e9n\u00e8r\u00e9 avec le programme de calibration. { \"Main\": { \"x\": 40, \"y\": 230, \"width\": 1845, \"height\": 719, \"Zones\": [ { \"DriverZone\": { \"x\": 0, \"y\": 3, \"width\": 1845, \"height\": 35, \"Windows\": [ { \"Position\": { \"x\": 2, \"y\": 0, \"width\": 32 }, \"GapToLeader\": { \"x\": 204, \"y\": 0, \"width\": 96 }, \"LapTime\": { \"x\": 413, \"y\": 0, \"width\": 105 }, \"Drs\": { \"x\": 526, \"y\": 0, \"width\": 81 }, etc... } ] } } ] } } Dans le futur il faudrait ajouter d'autres choses comme par exemple les diff\u00e9rents pilotes pr\u00e9sents sur le Grand Prix et ce genre d'infos. Quoique je vais l'ajouter d\u00e9ja maintenant et plus tard je mettrai en place la feature acessible depuis l'interface. Mais le hardcoder maintenant me permet d\u00e9ja de mieux coder l'autre c\u00f4t\u00e9. Ce programme n'est en aucun cas termin\u00e9 et je vais devoir travailler encore un peu dessus pour qu'il soit utilisable correctement mais au moins il fonctionne \u00e0 peu pr\u00e8s. Exemple du json avec les noms de pilotes: { \"Main\": { \"x\": 37, \"y\": 238, \"width\": 1851, \"height\": 713, \"Zones\": [ { \"DriverZone\": { \"x\": 0, \"y\": -5, \"width\": 1851, \"height\": 35 } } ] }, \"Drivers\": [ \"Leclerc\", \"Verstappen\", etc... ] } Maintenant je vais m'attaquer au d\u00e9codage. Demain je dois finir le d\u00e9codage du JSON et je dois commencer \u00e0 impl\u00e9menter la reconnaissance des textes. Voire m\u00eame des pneus etc si j'y arrive.","title":"Mardi 4 Avril"},{"location":"jdb.html#mercredi-5-avril","text":"Bon la il faut vraiment que je finisse assez vite la lecture du JSON et la reconstruction des zones pour commencer la reconnaissance. J'ai pris beaucoup de temps \u00e0 faire le programme de calibration mais je pense que c'est essentiel de prendre ce temps maintenant. (BTW il faudra quand m\u00eame retourner faire une plus jolie version par ce que la ca marche mais c'est tout) Bon apr\u00e8s pas mal de boulot je pense avoir r\u00e9ussi. Dans le nouveau programme on arrive \u00e0 r\u00e9cup\u00e8rer les diff\u00e9rentes zones : \"JSON decode result\" Un conseil de notre professeur M.Bonvin a \u00e9t\u00e9 de cr\u00e9er des Releases de versions qui ne fonctionnent pas ou pas tr\u00e8s bien. J'ai donc publi\u00e9 une premi\u00e8re release de l'OCR_TEST qui fonctionne vite fait. J'ai seulement un petit soucis, comme je recr\u00e9e compl\u00eatement la structure des driver zones avec seulement la premi\u00e8re, il y a un petit d\u00e9calage car entre les zones il y avait un gap. Ce qui fait que si la premi\u00e8re zone est parfaitement centr\u00e9e : \"Centered driver\" La vingti\u00e8me ne l'est plus exactement : \"Uncentered Driver\" Pour ca j'ai essay\u00e9 de mettre un espacement arbitraire mais c'est complexe. Je vais plut\u00f4t tenter de faire une diff\u00e9rence entre la taille de la zone compl\u00eate et de la taille additionn\u00e9e de toutes les fen\u00eatre et diviser le resultat entre toutes les fen\u00eatres. Ca n'est pas parfait mais au moins maintenant les donn\u00e9es ne touchent plus les bords de la fen\u00eatre. Et voila ! Maintenant avec le fichier de configuration en Json on arrive \u00e0 r\u00e9cup\u00e8rer toutes les infos comme si elles avaient \u00e9t\u00e9 envoy\u00e9es directement depuis l'app de calibration mais sans le processing time ! \"Verstappen folder 2 On peut donc ENFIN passer au d\u00e9codage de ces FICHUES donn\u00e9es. Je vais pouvoir impl\u00e9menter ce que j'ai fait dans le projet de test de d\u00e9codage. Gr\u00e2ce \u00e0 mon d\u00e9coupage initial qui m'a pris du temps \u00e0 impl\u00e9menter on a enfin un truc qui marche m\u00eame si je n'ai impl\u00e9ment\u00e9 que la reconnaissance de noms. \"Image reconnaissance propre\" Si on se rappelle du syst\u00e8me de window et de zones dans le diagramme plus haut, c'est assez facile de comprendre comment je m'y suis pris. En gros on des listes et des listes de listes de zones, c'est la partie un peu plus technique car il y a des zones qui peuvent contenir d'autres zones etc. Je vais commencer par la reconnaissance de noms. Voici le tableau de pilotes de 2023 \"Drivers\": [ \"Leclerc\", \"Verstappen\", \"Hamilton\", \"Alonso\", \"Russel\", \"Gasly\", \"Stroll\", \"Sainz\", \"Hulkenberg\", \"Norris\", \"Tsunoda\", \"Piastri\", \"Zhou\", \"Ocon\", \"Magnussen\", \"Perez\", \"Sargeant\", \"De Vries\", \"Bottas\", \"Albon\" ] ET voici le tableau de pilotes de 2022 : \"Drivers\": [ \"Leclerc\", \"Verstappen\", \"Sainz\", \"Perez\", \"Hamilton\", \"Russel\", \"Magnussen\", \"Gasly\", \"Ocon\", \"Alonso\", \"Tsunoda\", \"Bottas\", \"Zhou\", \"Albon\", \"Stroll\", \"Schumacher\", \"Hulkenberg\", \"Norris\", \"Latifi\", \"Ricciardo\" ] Je les notes ici car J'ai souvent besoin de changer selon le dataset que j'utilise. Dans le futur je ferai s\u00fbrement un grand dataset qui prend des pilotes de reserves et des pilotes juniors pour que dans le cas ou un pilote est remplac\u00e9 dans l'ann\u00e9e il n'y a pas besoin de tout recalibrer avec l'application. Apr\u00e8s une discussion avec M.Bonvin j'ai d\u00e9cid\u00e9 de tester 3 valeurs de convertion en noir et blanc et si je ne trouve pas un match exact je prend le nom le plus proche. Pour trouver la string la plus proche je pense que je vais utiliser quelque chose qui s'appelle la technique de Levenshtein. De ce que j'ai compris c'est un algorythme qui permet de donner une metric de diff\u00e9rence entre deux strings. Bon et \u00e9videmment il ne faut pas se tromper dans la liste des pilotes GENRE NE PAS OUBLIER QUE GEORGE RUSSELL COMPORTE DEUX WFNEWIEWV DE \"L\" A LA FIN DE SON NOM CE QUI POURRAIT ENGRANGER 2H DE DEBUGGING POUR RIEN ASK ME HOW I KNOW joker laugh J'ai vraiment un soucis avec Tsunoda, Il a trop tendeance \u00e0 le confondre avec \"TSUNDDA\" et pour des raisons obscures, quand j'applique l'algorythme de Levenshtein le plus proche n'est pas \"Tsunoda\" mais \"Sainz\" iniuvbwdiucbiubisc POURQUOI !!??!! Je pense que cela demande moins de changements de lettres enfin bon c'est quand m\u00eame pas id\u00e9al. Il va falloir que je trouve un moyen de le repond\u00e9rer. C'est dommage par ce que cela marche super avec Alonso Verstappen et Albon. J'ai un peu modifi\u00e9 la methode et j'ai fait en sorte d'envoyer tous les noms en majuscules en me disant que cela pourrait r\u00e9duire le nombre de changements. Et ca a march\u00e9 !! Cela va s\u00fbrement demander plus de tests pour \u00eatre bien s\u00fbr que tout fonctionne nikel, cependant pour le moment ca marche parfaitement avec les pilotes de 2022. Pour ce qui est de la reconnaissance de chiffres, j'ai d\u00e9ja fait une partie du boulot le premier jour alors je vais juste reprendre \u00e0 partir de l\u00e0. Je r\u00e9cup\u00e8re une string de ce type \"1:35.123\" le soucis c'est que les : se transforment parfois en . ou inversement mais bon ca devrait pas \u00eatre trop dur \u00e0 g\u00e8rer. Il faut que je transforme cette string en nombre de milisecondes (Du moins je pense que c'est le meilleur moyen pour ensuite pouvoir facilement comparer et stocker l'information). Cela fait que 1:35:123 en milisecondes donne : 1 * 1000 * 60 => 60'000 35 * 1000 => 35'000 123 => 123 Total : 60'000+35'000+123 => 95'123ms Et pour l'affichage : Minutes = ms / 60'000 secondes = (ms - (minutes/60'000))/1000 ms = ms - ((minutes 60'000) + (secondes * 1000)) Et on se retrouve avec 1:35:123 Maintenant apr\u00e8s un peu de temps pour nettoyer la string etc on se retrouve avec un r\u00e9sultat comme le suivant : Position : 0 Gap to leader : 0:0:0 Lap time : 2:15:123 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : LECLERC Sector 1 : 0:31:323 Sector 2 : 0:42:340 Sector 3 : 0:0:0 Evidemment pareil pour les autres pilotes Et je me rend compte que j'ai encore tout cass\u00e9 car le laptime ne devrait pas \u00eatre 2:15 mais 1:35... Voila apr\u00e8s une heure de debugging et des ajouts pour nettoyer les chaines on se retrouve avec : Position : 0 Gap to leader : 0:0:0 Lap time : 1:35:123 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : LECLERC Sector 1 : 0:31:323 Sector 2 : 0:42:340 Sector 3 : 0:0:0 Note: le traitement commence \u00e0 devenir long, il serait peut-\u00eatre int\u00e9ressant d'utiliser un seul Tesseract Engine ou de voir ce qui prend autant de temps, on d\u00e9passe la seconde de traitement ce qui est un peu ma limite. Apr\u00e8s on peut toujours tester de rajouter du multicore processing mais c'est pour une autre fois. Demain je m'occupe de r\u00e8gler les soucis que j'ai avec la prescision de ces temps au tour et j'\u00e9sp\u00e8re pouvoir m'occuper aussi de la position des pneus et du DRS. J'aimerais finir tout ca cette semaine.","title":"Mercredi 5 Avril"},{"location":"jdb.html#jeudi-6-avril","text":"Une id\u00e9e m'est pass\u00e9e par la t\u00eate pendant que je dormais, dans la liste des pilotes, quand ils sont \u00e0 plus d'un tour de retard avec le leader (Ce qui arrive normalement dans presque tous les Grand Prix) on a pas des minutes mais une string qui montre \"+1 Lap\" ou \"+2Laps\" ce qui est \u00e9videmment un probl\u00e8me. Je pense qu'une bonne facon d'envoyer l'info serait de retourner -1 -2etc... \u00e0 la place des milisecondes, mais encore faut-il detecter le nombre de tours Je devrais \u00eatre en train de commencer la documentation de commment tout ce que j'ai fait fonctionne. Cependant je ne me vois pas faire ca tant que je n'ai pas au moins r\u00e9cup\u00e8r\u00e9 toutes les infos au moins un peu proprement. Cela veut dire que je commence officiellement \u00e0 prendre du retard. (Sachant que si je finis tout aujourd'hui une journ\u00e9e de doc suffira largement le terme est un peu exag\u00e8r\u00e9 mais bon) Bon pour la reconnaissance des temps c'est sp\u00e9cial... Le filtre semble ne pas changer grand chose ce qui est probl\u00e9matique et ca n'est vraiment pas fiable. Voici quelques expemples avec un treshold de 100: \"11ZSD\" Cette image est comprise comme \"11ZSD\" 42340 Cette image est comprise comme \"42340\" \"ZZAEB\" Et celle ci \"ZZAEB\"... Ce qui... n'est pas bon du tout... J'ai essay\u00e9 de trouver un fichier d'entrainement sp\u00e9cifiquement fait pour les digits. J'ai essay\u00e9 de blacklister les chars non voulus pour tenter d'obliger Tesseract \u00e0 trouver des chiffres. Avec la premi\u00e8re option, les r\u00e9sultats ne sont pas meilleurs voire pires. Avec la seconde option c'est d\u00e9ja pas mal mieux mais on perd compl\u00e8tement la possibilit\u00e9 de detecter les mots comme \"LEADER\" ou \"LAP\" et de toute facon ca n'est pas parfait. Le soucis c'est que si je n'ai pas des donn\u00e9es fiables c'est juste impossible de faire des calculs et de l'affichage correct... Il faut absolument que je trouve une solution. J'ai essay\u00e9 d'utiliser de l'interpolation our augmenter la taille de l'image et ensuite appliquer mon filtre pour retirer le flou mais sans succ\u00e8s... Pourtant la on se retrouve avec des images plut\u00f4t claires : \"Clear1\" Ici le programme trouve \"44301\" \"Clear2\" Et ici \"A5151\"... On a toujours les m\u00eames probl\u00e8mes. Bon je suis all\u00e9 me renseigner sur l'OCR et je me suis dit que j'allais tenter de faire les choses proprement. Je vais faire passer plusieurs \u00e9tapes de postProcessing avant de donner l'image \u00e0 Tesseract. GrayScale Tresholding InvertColors Scaling Dilatation Ce qui donne : \"Original\" \"Grayscale\" \"InvertColors\" ; \"Resize\" ; \"Dilatation\" Ce qui ne change : Roulement de tambour RIEN kjd viuwvuirnvoirenbf Tout ca pour rien... C'EST BON !!! Bon en fait au final le probl\u00e8me \u00e9tait une mauvaise configuration de Tesseract. Je vais devoir un peu nettoyer tout ca. Mais avec les changements de l'image on a des r\u00e9sultats BEAUCOUP plus pr\u00e9cis et potentiellement utilisables. La je vais devoir faire un serieux travail de nettoyage et simplification de mon code par ce que la c'est vraiment un chantier vu le nombre de choses que j'ai du essayer. J'ai du aussi beaucoup modifier la gestion de l'image ce qui donne : \"Clean\" Et la on a des r\u00e9sultats qui sont vraiment bons. J'ai pu ajouter assez facilement la detection de position comme c'est simplement un chiffre. On se retrouve maintenant avec ce genre de retours : Position : 1 Gap to leader : 8:33:51 Lap time : 2:19:123 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : LECLERC Sector 1 : 0:31:828 Sector 2 : 0:42:940 Sector 3 : 0:0:0 Position : 2 Gap to leader : 0:3:259 Lap time : 23:12:392 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : VERSTAPPEN Sector 1 : 0:38:119 Sector 2 : 0:0:0 Sector 3 : 0:0:0 Il ne manque plus que l'impl\u00e9mentation de la reconnaissance du DRS et des Pneus Et non... je viens de me rendre compte que mon programme a encore cass\u00e9 car le tap time ne peut pas \u00eatre 23 min lol. J'ai un nouveau magnifique probl\u00e8me... Les points et les deux points sont interpr\u00eat\u00e9s comme des chiffres ... Give me a F * * break... J'ai du mal \u00e0 comprendre pourquoi ils ne sont d\u00e9tect\u00e9s comme tels que maintenant. Bon alors il semblerait les temps au tour aie besoin d'un ordre tr\u00e8s pr\u00e9cis pour fonctionner. Grayscale InvertColors Tresholding Resize * 2 Resize * 2 Et la on a des r\u00e9sultats un peu mieux. Bon demain il faut absolument que je me charge de r\u00e8gler tous ces probl\u00e8mes et que je commence la reconnaissance des pneus et de DRS par ce que je commence \u00e0 \u00eatre en retard.","title":"Jeudi 6 Avril"},{"location":"jdb.html#vendredi-6-avril-2023","text":"Alors aujourd'hui c'est le dernier jour avant de commencer \u00e0 \u00eatre en retard pour de bon. J'ai r\u00e9ussi \u00e0 r\u00e8gler le probl\u00e8me des temps au tour, des gaps, et des secteurs. Dans le processus j'ai cass\u00e9 la detection de position mais ca devrait pas \u00eatre TROP compliqu\u00e9. Et voila ... Apr\u00e8s seulement plus de dix heures de gal\u00e8re, si on donne cette image au programme et le bon JSON le programme nous retourne : Position : 1 Gap to leader : 0:05:059 Lap time : 1:39:123 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : LECLERC Sector 1 : 0:31:828 Sector 2 : 0:42:940 Sector 3 : 0:00:000 Position : 2 Gap to leader : 0:03:259 Lap time : 1:39:392 DRS : False Tyre : Undefined laps with the tyre : 0 Driver name : VERSTAPPEN Sector 1 : 0:31:749 Sector 2 : 0:00:000 Sector 3 : 0:00:000 Evidemment le GapToLeader est faux sur leclerc car il est leader mais bon ca je pourrai toujours Hardcoder que le premier a jamais de GapToLeader. Bon j'ai eu beaucoup de soucis que je ne vais pas mentionner ici car ce sont simplement des soucis de logique de programmation pour trouver un DRS ouvert ou non. Au final la technique que j'utilise et qui marche plut\u00f4t bien pour le DRS est que je prend la premi\u00e8re image de DRS et je la d\u00e9clare comme valeur \u00e9talon d'un DRS non actif, en effet dans 99% des cas le leader n'a pas de DRS (cela peut arriver alors il faudra donc juste verifier que les pilotes sont bien \u00e0 moins de deux secondes les uns des autres pour confirmer). Ensuite cette valeur \u00e9talon je la calcule en fonction du nombre de pixels verts dans l'image et si il y a plus de 30% de pixels verts en plus c'est que le DRS est activ\u00e9 ex: Ceci est un DRS ferm\u00e9: \"Closed DRS\" Ceci est un DRS ouvert: \"Open DRS\" Cela marche \u00e0 peu pr\u00e8s tout le temps mais dans le pire des cas on peut toujours verifier que les pilotes sont bien proches pour detecter les potentiels rares cas de faux positifs. J'ai pu augmenter les performances en utilisant un seul engine pour tout le monde et en arr\u00eatant d'utiliser GetPixel et SetPixel qui sont simplement des horreurs \u00e0 utiliser. Mais elles ne sont pas encore bonnes Le soucis avec la detection de pneus cependant, c'est qu'il n'est pas possible d'utiliser la reconnaissance pour savoir ou regarder la couleur car cela ne marcherait pas. Je ne peux pas faire trop de post processing car je dois conserver la couleur Je ne peux pas hardocder un endroit ou aller regarder car cela \u00e9volue tout le long du Grand Prix. Bref c'est la gal\u00e8re. En y r\u00e9flechissant je me suis dit qu'une bonne id\u00e9e pourrait \u00eatre de partir de la droite de la zone du pneu en regardant au milieu de la hauteur. Puis continuer vers la gauche jusqu'\u00e0 ce que je rencontre une couleur diff\u00e9rente. Je pourrai ensuite faire une zone un peu vers la gauche qui devrait contenir les infos du pneu et sur laquelle il sera possible de faire de le reconnaissance de couleur et de la reconnaissance de chiffres. J'ai d\u00e9termin\u00e9 que le background n'\u00e9tait jamais plus clair que #505050 et que donc nimporte quelle couleur qui aurait plus que 50 dans un seul des channels serait consid\u00e8r\u00e9e comme une couleur cassant le background Pour arriver \u00e0 cette conclusion je me suis amus\u00e9 un peu avec les couleurs pour jouer avec les limites de mon algorythme : \"Color fun\" Et je crois que j'ai eu une bonne id\u00e9e, avec une petite methode bien faite on arrive \u00e0 de supers r\u00e9sultats : private Rectangle FindTyreZone() { Bitmap bmp = WindowImage; int currentPosition = bmp.Width; int height = bmp.Height / 2; Color limitColor = Color.FromArgb(0x50,0x50,0x50); Color currentColor = Color.FromArgb(0,0,0); Size newWindowSize = new Size(bmp.Height,bmp.Height); while(currentColor.R <= limitColor.R && currentColor.G <= limitColor.G && currentColor.B <= limitColor.B && currentPosition > 0) { currentPosition--; currentColor = bmp.GetPixel(currentPosition,height); } //Its here to let the new window include a little bit of the right side int offset = Convert.ToInt32((float)newWindowSize.Width / 100f * 20f); int CorrectedX = currentPosition - (newWindowSize.Width - offset); if (CorrectedX <= 0) return new Rectangle(0,0,newWindowSize.Width,newWindowSize.Height); return new Rectangle(CorrectedX,0,newWindowSize.Width,newWindowSize.Height); } \"Tyres\" Maintenant cela devrait \u00eatre beaucoup plus simple de trouver la couleur g\u00e9n\u00e9rale et le nombre de tours. Donc ce que je fais c'est que je fais une reconnaissance de texte sur l'image r\u00e9duite. Si je trouve une lettre c'est facile Ca me donne le type de pneu et ca me dit que c'est le premier tour avec. Si c'est un nombre alors je fais la moyenne de toutes les couleurs de l'image et je prend la couleur de pneu la plus proche. Voici les diff\u00e9rentes couleurs de pneus : SOFT : #FF0000 MEDIUM : #f5bf00 HARD : #d9d8d4 INTER : #00a42e WET : #2760a6 \"Tyre colors\" Les couleurs de pneus peuvent changer de temps \u00e0 autres, par exemple cette r\u00e8gle de pneus est arriv\u00e9e en 2019 et avant il y avait beaucoup plus de couleurs mais dans une volont\u00e9 de rendre le sport plus facile \u00e0 comprendre \u00e0 la t\u00e9l\u00e9 cela a \u00e9t\u00e9 simplifi\u00e9. Je ne pense pas que cela va changer dans les ann\u00e9es qui viennent alors tout est hardcod\u00e9. Je pense que j'ai des soucis avec la detection de texte et de couleur car ma zone est trop grande. Alors bon j'\u00e9crit ces lignes apres des heures de tests. Il semble que la principale difficult\u00e9 avec ces pneus c'est que les chiffres ou lettres sont minuscules. Il est donc extr\u00eamement difficile de faire une reconnaissance ne serait-ce qu'un peu fiable.. Je fais de mon mieux pour tenter de r\u00e8gler le soucis cependant c'est vraiment complexe. Je commence \u00e0 devenir fou, je tente tout et nimporte quoi pour permettre \u00e0 mon algo de fonctionner et m\u00eame quand je fais du post processing comme pas possible il me retourne toujours nimporte quoi... \"5i t'inqui\u00e8tes\" Ici le programme va trouver '5i'... En fait c'est complexe d'expliquer tout ce que je fais car je change tout en boucle en essayant et en ratant ce qui prend des heures. Pour aujourd'hui j'abandonne je vais simplement rentrer chez moi et y r\u00e9flechir cette nuit mais je ne vois pas comment mieux faire la... C'est terrible par ce que je sens que je ne suis pas bien loin.","title":"Vendredi 6 Avril 2023"},{"location":"jdb.html#vacances","text":"Bon je vais un peu laisser de c\u00f4t\u00e9 la detection de chiffres pour me pencher un peu plus sur la d\u00e9tection de couleur. Par ce que techniquement si j'arrive \u00e0 toujours parfaitement la detecter alors je pourrais me passer des chiffres car ils sont redondant si je construit un historique de pneus. J'ai r\u00e9ussi \u00e0 fix mon probl\u00e8me de mauvaise detection de couleur de pneus. Du moins je crois. Seulement j'ai quand m\u00eame un souci, les fen\u00eatres de pneus avec une lettre n'ont pas assez de couleur pour \u00eatre d\u00e9tect\u00e9s. Je vais donc essayer de detecter les cinq lettres possibles et si il ne trouve pas alors je pourrai tenter de detecter les chiffres sans lettres ce qui devrait grandement aider. Le but est encore une fois de r\u00e9duire les possibilit\u00e9s de Tesseract. Je me rend de plus en plus compte que le plus important c'est de r\u00e9duire le scope le plus possible. Moins il y a de mots et lettres et de chiffres possibles meilleure sera la reconnaissance. Bon ca ne veut toujours pas marcher maintenant le 11 est interpr\u00eat\u00e9 comme trois I ou comme un M... J'en ai marre sans rire c'est vraiment p\u00e9nible. Alors j'\u00e9crit ces lignes deux jours plus tard et me rend compte avec horreur que toutes mes modifications sur ce journal de bord n'ont pas \u00e9t\u00e9 auvegard\u00e9e... yess.. Bon pour faire simple, j'ai r\u00e9ussi \u00e0 rendre la detection de couleurs bien plus efficace en r\u00e9duisant la taille de l'image et en ne prenant pas en compte les couleurs que l'on d\u00e9tecte comme \u00e9tant partie int\u00e9grante du background. Par exemple quand on a une image comme celle ci : \"Avec background\" qui contient un background alors que ci dessous, on l'a enlev\u00e9. \"Sans background\" La diff\u00e9rence est t\u00e9nue mais elle permet de grandement am\u00e9liorer la prescision de la reconnaissance de couleurs. Pour ce qui est du nombre de tours je me suis rendu compte que cela n'\u00e9tait d\u00e9ja pas tr\u00e8s utile car avec l'historique on devrait pouvoir le d\u00e9duire. Mais bon pour la forme je me suis dit que cela serait quand m\u00eame une bonne id\u00e9e de v\u00e9rifier avec la reconnaissance. J'\u00e9tais quasi certain que le soucis \u00e9tait le fait que l'on voie le contour du logo de pneu qui faisait que la reconnaissance avait du mal. Et j'avais raison ! En les enlevant (Ce qui n'a pas \u00e9t\u00e9 simple) J'ai pu avoir des chiffres beaucoup plus proches de la r\u00e9alit\u00e9. En m\u00eame temps je ne vois pas bien comment j'aurais pu faire mieux : \"Super 11\" Je suis quand m\u00eame assez fier de voir que j'ai r\u00e9ussi \u00e0 part de l'image que on peut voir un peu plus haut et automatiquement la transormer en celle ci-dessus. J'ai donc pu retirer le round autour du chiffre et cela m'a permit de pouvoir d\u00e9zoomer un peu et c'est avec ca que les lettres ont pu \u00eatre mieux reconnues : \"Super H\" \"Super M\" Maintenant je pense qu'il ne reste \"plus qu'\u00e0\" nettoyer un peu tout ce code qui traine de partout pour tout faire fonctionner et impl\u00e9menter un peu de parrallel processing ainsi que de l'asynchrone pour ne pas bloquer le reste du programme. Par ce qu'il faut savoir que en l'\u00e9t\u00e2t, le programme met 25 secondes \u00e0 d\u00e9marrer et consomme presque 2GB de Ram. Certes cela ne veut pas dire que la reconnaissance \u00e0 elle seule prend 25 secondes car au d\u00e9marrage il y a aussi la lecture du fichier de config et la cr\u00e9ation des window etc.. En r\u00e9alit\u00e9 la partie strictement OCR prend dans les 12s si on en croit la fonction stopWatch de C#. Et quand on change d'image la reconnaissance prend 9s. Dans tout les cas c'est BEAUCOUP trop. J'aurais eu comme objectif de faire une reconnaissance toutes les secondes. Je ne sais pas bien si cela va \u00eatre possible mais en tout cas le but va \u00eatre de s'en rapprocher. Pour \u00eatre plus exact et permettre une comparaison, voici les stats exactes Avec un fichier d'images vide : Loading - 11.8s Splitting d'images - 90ms OCR - 12.5s Avec un fichier d'images plein : Loading - 10.8s Splitting d'images - 80ms Ocr - 11.6s En passant d'une image \u00e0 l'autre : Loading - NaN Splitting d'images - 50ms Ocr - 8.8s Donc on peut voir que les deux endroits ou le programme prend le plus de temps c'est au premier d\u00e9marrage quand il faut lire le fichier et setup les windows etc... Et l'OCR qui prend un temps fou. Ce qui est pratique c'est que les presque 2gb de ram sont utilis\u00e9s que au lancement et ensuite l'application n'en utilise que quelques centaines de mb. Le processeur lui tourne entre 10 et 20% ce qui ne va pas durer :) Je vais m'occuper dabord du loading. J'ai essay\u00e9 d'utiliser un Parrallel.For au moment de la cr\u00e9ation des windows, le probl\u00e8me c'est que visiblement les objets windows sont beaucoup trops complexes et utilisent trop de ressources partag\u00e9es pour \u00eatre vraiment thread Safe. J'\u00e9sp\u00e8re que je n'aurais pas trop de soucis avec ca qu'en j'en viendrai \u00e0 l'optimisation de l'OCR... Ce qui me rend fou c'est que cette boucle toute nulle prend plus de dix secondes \u00e0 s'executer et je ne comprend pas bien pourquoi. for (int i = 0; i < NUMBER_OF_DRIVERS; i++) { Point tmpPos = new Point(0, FirstZonePosition.Y + i * FirstZoneSize.Height - Convert.ToInt32(i * offset) /*- (i* (FirstZoneSize.Height / 32))*/); Zone newDriverZone = new Zone(MainZoneImage, new Rectangle(tmpPos, FirstZoneSize)); Bitmap zoneImg = newDriverZone.ZoneImage; newDriverZone.AddWindow(new DriverPositionWindow(zoneImg, new Rectangle(driverPositionPosition, driverPositionArea))); newDriverZone.AddWindow(new DriverGapToLeaderWindow(zoneImg, new Rectangle(driverGapToLeaderPosition, driverGapToLeaderArea))); newDriverZone.AddWindow(new DriverLapTimeWindow(zoneImg, new Rectangle(driverLapTimePosition, driverLapTimeArea))); newDriverZone.AddWindow(new DriverDrsWindow(zoneImg, new Rectangle(driverDrsPosition, driverDrsArea))); newDriverZone.AddWindow(new DriverTyresWindow(zoneImg, new Rectangle(driverTyresPosition, driverTyresArea))); newDriverZone.AddWindow(new DriverNameWindow(zoneImg, new Rectangle(driverNamePosition, driverNameArea))); newDriverZone.AddWindow(new DriverSector1Window(zoneImg, new Rectangle(driverSector1Position, driverSector1Area))); newDriverZone.AddWindow(new DriverSector2Window(zoneImg, new Rectangle(driverSector2Position, driverSector2Area))); newDriverZone.AddWindow(new DriverSector3Window(zoneImg, new Rectangle(driverSector3Position, driverSector3Area))); MainZone.AddZone(newDriverZone); } Alors que Zone.AddWindow c'est simplement : public virtual void AddWindow(Window window) { Windows.Add(window); } Et windows est simplement une liste. Donc ca ne peut pas \u00eatre ca qui prend du temps. Et les windows que je cr\u00e9\u00e9 ont ca comme code : public DriverPositionWindow(Bitmap image, Rectangle bounds) : base(image, bounds) { Name = \"Position\"; } Sachant que le constructeur de base d'une Window c'est : public Window(Bitmap image, Rectangle bounds) { Image = image; Bounds = bounds; Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, \"eng\", EngineMode.Default); Engine.DefaultPageSegMode = PageSegMode.SingleLine; } Sachant que TesseractEngine est en statique et que donc il ne devrait... OHLLALALALALALALALALA je suis un imb\u00e9cile... J'ai juste \u00e0 changer ce constructeur avec ca: if (Engine == null) { Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, \"eng\", EngineMode.Default); Engine.DefaultPageSegMode = PageSegMode.SingleLine; } ET le loading ne prend plus que 2-300 ms... Bon c'est une tr\u00e8s belle am\u00e9lioration pour pas tr\u00e8s ch\u00e8r mais bon c'est un peu b\u00eate... Bon je pense que 2-300ms c'est une dur\u00e9e correcte surtout que ca n'est appel\u00e9 qu'une fois pour le lancement. On peut passer \u00e0 la suite maintenant. Alors il y a un grand soucis avec la parallellisation de l'OCR... Tesseract n'est pas par d\u00e9faut une classe \"Thread safe\" ce qui veut dire que je ne peut utiliser de parallell.Foreach sur mes windows pour acc\u00e8l\u00e8rer le traitement drastiquement. Je pourrais par exemple avoir une instance de Tesseract par window sauf que cela fait 20 pilotes * 9 windows chacuns ce qui donne 180 instances ce qui n'est tout simplement pas raisonnable. Je vais donc essayer de voir avec l'utilisation de methodes asynchrones qui me permettraient de faire un genre de flux tendu de reconnaissance. J'avoue que la je navigue un peu \u00e0 vue, je me base sur diff\u00e9rentes infos que je trouve sur des sites un peu perdus et sur chatGPT, j'esp\u00e8re que j'arriverai \u00e0 trouver une solution car 10 secondes de reconnaissance c'est vraiment beaucoup trop. Alors le soucis avec un Engine unique entre toutes les windows c'est qu'il n'est pas possible de process plusieurs images \u00e0 la fois. Je vais donc retirer l'engine unique pour voir si en cr\u00e9er un par window me permet de passer en multithreading. La grande question sera : Est-ce que les ressources suppl\u00e9mentaires que vont prendre la cr\u00e9ation de tous ces engines va compenser enti\u00e8rement le temps gagn\u00e9 avec la paralellisation. Pour stocker les donn\u00e9es dans un premier temps je vais cr\u00e9er un objet DriverData. Ce qu'il y a de pratique avec ca, c'est que je pourrais ajouter du code de v\u00e9rification de certaines donn\u00e9es directement dedans avant de les donner \u00e0 la suite du programme. Et on peut m\u00eame imaginer une impl\u00e9mentation d'une liste de DriverData pour avoir l'historique. Ce qui serait cool ca serait de grouper toutes ces data avec un num\u00e9ro de tour. Placer ensuite la liste de Data dans une DB serait ainsi super simple. Mais il va falloir savoir quoi mettre, quelles infos sont redondantes et prendre en compte le fait que un tour affich\u00e9 sur la page de la F1TV n'est accompli que par certains des premiers pilotes. D'autres pilotes peuvent \u00eatre dans des tours pr\u00e9c\u00e9dents si ils ont du retard. Il faudra r\u00e9fl\u00e9chir \u00e0 cela quand je viendrai au mod\u00e8le. Bon pour y arriver j'ai du faire de gros changements et le r\u00e9sultat n'est peut-\u00eatre pas aussi cool que ce que j'aurais voulut... Voici un petit point sur les performances maintenant J'ai \u00e9galement d\u00e9sactiv\u00e9 le dump d'images. Pour le moment j'ai tout mis en commentaire mais cela pourrait \u00eatre int\u00e9ressant de faire en sorte de pouvoir l'activer en changeant une ou deux variables Au d\u00e9marrage : Loading - 113ms Splitting d'images - 14ms Ocr - 7s En passant d'une image \u00e0 l'autre : Loading - 113ms Splitting d'images - 13ms Ocr - 5s Alors clairement les stats montrent qu'il y a eu un changement mesurable mais bon je pensais pouvoir en gagner un peu plus... Je soupconne la cr\u00e9ation d'engines d'\u00eatre \u00e0 l'origine de ces performances presque d\u00e9cevantes. Autre soucis, il semble que plus je change d'image plus la detection est lente et plus je consomme de RAM. Il va falloir que je travaille encore un peu. J'ai tent\u00e9 de mettre un stopwatch sur une des cr\u00e9ations d'engine Tesseract et le r\u00e9sultat me parait fou... Plus d'une seconde c'est dingue. J'ai test\u00e9 dans d'autres endroits du code et effectivement il semble que la cr\u00e9ation d'un engine prenne entre une et deux secondes ce qui est une ETERNITEE what ! Donc il faut optimiser tout ca. Une id\u00e9e serait de d\u00e9composer le threading mais cela me demanderait un gros refactor et je n'ai pas envie d'en refaire un la... Sinon, une fois qu'ils sont cr\u00e9\u00e9s ils ne prennent pas de temps du tout. Cr\u00e9er une fois tous les engines et ensuite les utiliser pourrait \u00eatre une bonne id\u00e9e. Cela prendrait longtemps au load mais ensuite les reconnaissances devraient \u00eatre super rapides. Ok alors ca c'est d\u00e9ja plus ce \u00e0 quoi je m'attendais ! On est de nouveau \u00e0 plus de 10s de loading time mais on est descendu \u00e0 deux secondes par OCR. (Bon autre soucis, l'utilisation de la RAM est ridicule plus de 2gb mais ce qui m'inqui\u00e8te c'est que j'ai l'impression qu'elle augmente plus on change d'image) J'ai r\u00e8gl\u00e9 (en partie) le soucis en obligeant le GC (Garbage Collector) \u00e0 collecter apr\u00e8s chaque detection. m\u00eame apr\u00e8s 50 detections l'utilisation de la ram se stabilize autour des 2GB. Bon en paralellisant la cr\u00e9ation des Engines le soucis c'est que cela demande d'allouer beaucoup trop de m\u00e9moire d'un coup alors le programme se fige pendant genre cinq secondes avant de tout cr\u00e9er. Du coup m\u00eame si la cr\u00e9ation est plus rapide, on se retrouve avec un temps total plus long... Je pense que l'on va devoir se contenter de ces dix secondes. Bon la j'allais tenter de faire la documentation mais je viens de me rendre compte que la detection de temps au tour est pas vraiment encore id\u00e9ale... J'ai r\u00e9ussi \u00e0 changer un petit peu le programme de reconnaissance pour rendre la reconnaissance un peu meilleure mais cela a drastiquement augment\u00e9 le temps requis pour d\u00e9coder... On arrive \u00e0 3.5 secondes. Je vais tenter de rajouter un peu de parralell processing sur les boucles de traitement voir si cela peut aider. Alors effectivement cela aide pas mal, on arrive maintenant \u00e0 faire une detection presque tout le temps en dessous de la seconde. Et j'ai aussi du changer un peu le fonctionnement de la detection des Temps au tour. Et voila je pense que je vais m'arr\u00eater la pour la partie d\u00e9codage. Je ne pense pas que je peux facilement faire mieux que ca et il faut que j'avance dans d'autres parties du projet. Je vais pouvoir commencer \u00e0 documenter un peu toute la partie OCR. Il faut que je prenne le temps de le faire bien car c'est la partie la plus int\u00e9ressante du projet et ou je pense que j'aurai le plus essay\u00e9 de choses qui vallent le coup d\u00eatres racont\u00e9es. J'ai aussi pass\u00e9 pas mal de temps sur le poster du projet. J'avais fait des croquis au crayon de ce \u00e0 quoi je pensais, cependant apr\u00e8s de longues discussions avec M.Garcia ils n'\u00e9taient pas forc\u00e9ment tr\u00e8s bons car ils ne repr\u00e9sentent pas assez bien le fonctionnement du projet et sont un peu trop marketings. Du coup j'ai fait une premi\u00e8re version au propre : \"Poster V1\" Mais je n'\u00e9tais pas forc\u00e9ment content du r\u00e9sultat et il manquait des choses je trouve comme par exemple l'utilisation de Selenium. J'ai donc repass\u00e9 des heures \u00e0 faire une seconde version : \"Poster V2\" La police d'\u00e9criture n'est pas encore la bonne mais cela va venir. Mais je pr\u00e9f\u00e8re d\u00e9ja beaucoup cette version \u00e0 la premi\u00e8re. Je ne sais pas encore si la version finale sera une version plus travaill\u00e9e de ce poster ou compl\u00eatement autre chose mais pour l'instant je suis \u00e0 peu pr\u00e8s content de cette version. Je le trouve un tout petit peu trop brouillon ou avec trop d'infos mais il m'a \u00e9t\u00e9 de nombreuses fois reproch\u00e9 de ne pas assez montrer le fonctionnememt interne et je ne peux pas faire plus simple. L'ajout des nombres pour compartimenter le projet ajoute de la structure mais je me demande si cela suffit. Maintenant que je suis \u00e0 peu pr\u00e8s content de mon code pour l'OCR je vais commencer sa documentation. (Uniquement son fonctionnement interne pas comment s'en servir car cela va changer) Bon j'ai cr\u00e9\u00e9 u nouveau projet selenium mais m\u00eame avec les bonnes libraries je n'arrivais pas \u00e0 faire fonctionner firefox j'avais toujours une erreur \"OpenQA.Selenium.WebDriverException: 'Cannot start the driver service on http://localhost:51481/'\" et j'ai pu r\u00e8gler le probl\u00e8me en t\u00e9l\u00e9chargeant directement le gecko driver depuis le git https://github.com/mozilla/geckodriver/releases et utiliser le fichier directement dans le service : var service = FirefoxDriverService.CreateDefaultService(AppDomain.CurrentDomain.BaseDirectory+@\"geckodriver-v0.27.0-win32\\geckodriver.exe\"); FirefoxOptions options = new FirefoxOptions(); var driver = new FirefoxDriver(service,options); Le seul probl\u00e8me c'est que du coup il faut tout le temps d\u00e9placer le fichier dans le dossier bin si je clone le projet. Il faudra faire un installeur dans la version finale qui s'occupe de tout je pense. Je me suis dit que j'allais garder la doc pour le retour des vacances quand j'aurai un bureau un clavier et un setup complet un peu propres. Bon il va falloir que je parle de la r\u00e9cup\u00e9ration de cookie. J'ai d\u00e9ja pu travailler lors d'un poc sur la meilleure facon de prendres des screenshots de la F1TV : Avoir une page chrome ouverte avec le feed en plein \u00e9cran et un programme qui prend des captures d'\u00e9crans. Avoir une cam\u00e9ra qui prend en photo l'\u00e9cran au cas ou chrome et Firefox emp\u00eachent la prise de captures d'\u00e9crans. R\u00e9cup\u00e8rer directement le feed en faisant du reverse engeneering de la plateforme. Simuler un chrome en background qui prenne des screenshot sans qu'on aie \u00e0 le voir. Dans toutes ces options, je dirais que la pire \u00e9tait celle de la cam\u00e9ra qui filme l'\u00e9cran, mais \u00e0 l'\u00e9poque c'\u00e9tait encore envisageable comme solution de dernier recours. Le soucis de cette solution c'est l'horreur que serait la partie OCR avec une image de tr\u00e8s mauvaise qualit\u00e9. Une autre option qui m'aurait vraiment emb\u00eat\u00e9 aurait \u00e9t\u00e9 de devoir garder une page de Chrome ou Firefox ouverte quelque part sur un \u00e9cran pour que le programme puisse prendres des captures d'\u00e9crans. C'est de loin l'option la plus simple et la plus logique mais elle poss\u00e8de pour moi de tr\u00e8s gros points noirs : On ne peut pas certifier l'int\u00e9grit\u00e9 des donn\u00e9es car l'utilisateur a le contr\u00f4le total sur le feed. Il peut mettre pause, avancer, reculer, tout casser sans faire expr\u00e8s en ouvrant autre chose sur son ordi qui se mette pile devant. Bref c'est un peu bancale. Et surtout on bloque une partie non significative de l'\u00e9cran de l'utilisateur avec des infos redondantes. Et je peux vous dire que quand je commente la F1 j'ai besoin de beaucoup d'informations et que chaque centim\u00e8tre d'\u00e9cran est crucia\u00e9 ! Alors avoir un \u00e9cran complet bloqu\u00e9 est juste un point bloquant qui m'emp\u00eacherait d'utiliser l'app aussi bonne soit-elle dans ses pr\u00e9dictions. Mais bon si aucune autre methode ne fonctionne ce qui est bien c'est que celle la est plut\u00f4t simple \u00e0 mettre en place. Ensuite reverse engeneer le feed serait l'option la plus classe, cependant c'est la plus complexe et la plus bancale au niveau l\u00e9gal haha. L'id\u00e9e serait de r\u00e9cup\u00e8rer le lien vers le broadcast g\u00e9n\u00e9ral et de comprendre comment il fonctionne pour le d\u00e9coder nous m\u00eame pendant un Grand Prix. Seuls soucis : Il n'est pas possible de faire des tests en dehors des periodes de Grand Prix (Et je rappelle que c'est des p\u00e9riodes ou je travaille en plus) Difficile de faire un syst\u00e8me qui marche pareil pour les rediffusions et les lives. (En effet les liens des rediff sont beaucoup plus simple \u00e0 r\u00e9cup\u00e8rer mais ne fonctionnent pas du tout pareil et pour tester l'app il est essentiel de pouvoir s'entrainer sur des anciens Grand Prix) Dernier GROS soucis, je ne sais tout simplement pas faire ca lol. Je ne sais pas comment faire. Peut-\u00eatre que avec des profs qui m'aident et chat gpt ainsi qu'internet je pourrais potentiellement n\u00e9gocier un truc mais c'est hautement improbable et cela serait une perte de temps folle si je n'y arrive pas. Derni\u00e8re option que je trouve la plus s\u00e9duisante. Simuler une instance de Chrome ou de Firefox (Le soucis avec chrome c'est qu'il impl\u00e9mente l'utilisation de DRM dans les vid\u00e9os qui fait qu'il est tr\u00e8s difficile de passer outre la s\u00e9curit\u00e9 avec un bot) pour ensuite prendre des captures d'\u00e9crans automatiquement. Cette solutions offre pleins d'avantages : Pas de place prise sur l'\u00e9cran L'int\u00e9grit\u00e9 des donn\u00e9es est assur\u00e9e car c'est le programme qui d\u00e9cide d'ou partir et de si il met pause ou non C'est une option complexe mais beaucoup moins que le reverse engeneering Elle permet de ne demander presque aucun input de la part de l'utilisateur. Mais elle pose quelques probl\u00e9matiques : Comment se connecter automatiquement sans \u00eatre detect\u00e9 par un Bot et sans demander \u00e0 l'utilisateur ses identifiants (Pour des raisons \u00e9videntes qui sont : QUI VA METTRE SES IDENTIFIANTS SUR UNE VIEILLE APP COMME LA MIENNE??) Comment faire en sorte que le programme prenne les meilleures captures dans la meilleure qualit\u00e9 et en plein \u00e9cran. Mais j'ai d\u00e9cid\u00e9 de partir sur cette option. Pour ce faire j'utilise Selenium. J'ai pu tester Puppetteer Sharp et m\u00eame si dans un premier temps j'ai pu avancer asez vite, malheureusement il y a des bugs qui rendent son utilisation impossible dans notre contexte. J'ai donc d\u00e9cid\u00e9 de tout faire en utilisant un portage de Selenium dans mon programme. Voici un exemple de code qui va ouvrir FireFox et qui va lancer un RickRoll var service = FirefoxDriverService.CreateDefaultService(AppDomain.CurrentDomain.BaseDirectory+@\"geckodriver-v0.27.0-win32\\geckodriver.exe\"); service.Host = \"127.0.0.1\"; service.Port = 5555; FirefoxOptions options = new FirefoxOptions(); options.AddArgument(\"--disable-headless\"); var driver = new FirefoxDriver(service,options); driver.Navigate().GoToUrl(\"https://www.youtube.com/watch?v=dQw4w9WgXcQ&autoplay=1&mute=1\"); Dans cet exemple on d\u00e9sactive le \"Headless\" pour qu'on puisse voir ce que fait l'app car sinon tout est invisible. Alors dans les faits la vid\u00e9o youtube ne se lance pas du tout car il y a des pubs et des prompts de cookies que l'on doit accepter etc... ce qui montre les diff\u00e9rents challenges que l'on va devoir surmonter pour vraiment faire ce que l'on veut. Mais un petit d\u00e9tail extr\u00eamement important, la F1TV est un programme payant un peu comme netflix. Ce qui veut dire que pour acc\u00e8der au contenu il faut \u00eatre connect\u00e9. Sauf que une instance de firefox cr\u00e9\u00e9 par Selenium est comme une page de naviguation priv\u00e9e, ce qui veut dire que si on va sur la page de la F1TV on est pas connect\u00e9s. Je pourrais tout \u00e0 fait demander \u00e0 l'utilisateur de me donner ses identifiants pour que j'aille ensuite automatiquement me connecter sauf que cela pose deux soucis: Personne ne voudra mettre ses identifiants sur mon programme La page de login de la F1TV a \u00e9t\u00e9 prot\u00e8g\u00e9e avec la meilleure technologie de detection de bots que je connaisse. Presque aucun site n'arrive \u00e0 me detecter sauf eux ! Donc c'est tout simplement impossible d'utiliser cette technique. Ensuite je me suis rappel\u00e9 que ce que la page stocke pour me permettre de rester connect\u00e9 ce sont des cookies. Et si je mets le bon cookies dans Selenium alors je serai connect\u00e9. Dans un premier temps je voulais faire un syst\u00e8me ou l'utilisateur irait prendre dans son chrome son cookie et le copie colle dans mon programme mais c'est immonde. C'est alors que vient la partie r\u00e9cup\u00e8ration de cookies ! Tous les cookies de chrome sont stock\u00e9s dans une base de donn\u00e9es SQLITE. On pourrait se dire Banco il suffit d'aller dedans et de retrouver tous les cookies et se connecter. Sauf que, pas b\u00eates, les \u00e9quipes de chrome ont d\u00e9cid\u00e9 que c'\u00e9tait une bonne id\u00e9e d'encoder les cookies pour que tout le monde ne puisse pas venir y mettre son nez... En effet les cookies peuvent contenir des informations importantes. Cela fait que pour utiliser ces cookies il faut pouvoir les d\u00e9coder. Mon hypoth\u00e8se a \u00e9t\u00e9 que si ces cookies peuvent \u00eatre lus par Chrome m\u00eame hors connexion, c'est que la cl\u00e9 de d\u00e9codage existe sur l'appareil et qu'il suffit de la trouver. ET C'EST LE CAS! Apr\u00e8s pas mal de recherches j'ai pu voir que la cl\u00e9 de d\u00e9codage existe bel et bien et qu'il suffit de la d\u00e9coder en utilisant la librairie DPAPI pour la lire. Avec cette cl\u00e9 on peut ensuite d\u00e9coder les cookies et leurs valeurs ce qui veut dire qu'il est th\u00e9oriquement possible d'automatiser le processus sans que l'utilisateur n'aie rien \u00e0 faire. J'ai d\u00e9cid\u00e9 de faire la partie r\u00e9cup\u00e8ration en python pour deux raison : Je n'arrivais pas \u00e0 trouver une bonne impl\u00e9mentation de DPAPI en C# qui me permettait de d\u00e9coder la cl\u00e9. Il existe beaucoup plus de documentation en Python pour ce qui est de la cryptographie et donc si Chrome change de fonctionnement il sera beaucoup plus simple de changer cette partie en particulier sans avoir \u00e0 recompiler le code C#. J'ai donc avec l'aide d'internet et de ChatGPT cr\u00e9\u00e9 ce script : def get_master_key(): with open( os.getenv(\"localappdata\") + \"\\\\Google\\\\Chrome\\\\User Data\\\\Local State\", \"r\" ) as f: local_state = f.read() local_state = json.loads(local_state) master_key = base64.b64decode(local_state[\"os_crypt\"][\"encrypted_key\"]) master_key = master_key[5:] # removing DPAPI master_key = win32crypt.CryptUnprotectData(master_key, None, None, None, 0)[1] print(\"MASTER KEY :\") print(master_key) print(len(master_key)) return master_key def decrypt_payload(cipher, payload): return cipher.decrypt(payload) def generate_cipher(aes_key, iv): return AES.new(aes_key, AES.MODE_GCM, iv) def decrypt_password(buff, master_key): try: iv = buff[3:15] payload = buff[15:] cipher = generate_cipher(master_key, iv) decrypted_pass = decrypt_payload(cipher, payload) decrypted_pass = decrypted_pass[:-16].decode() # remove suffix bytes return decrypted_pass except Exception: # print(\"Probably saved password from Chrome version older than v80\\n\") # print(str(e)) return \"Chrome < 80\" master_key = get_master_key() cookies_path = Path( os.getenv(\"localappdata\") + \"\\\\Google\\\\Chrome\\\\User Data\\\\Default\\\\Network\\\\Cookies\" ) if not cookies_path.exists(): raise ValueError(\"Cookies file not found\") with sqlite3.connect(cookies_path) as connection: connection.row_factory = sqlite3.Row cursor = connection.cursor() cursor.execute(\"SELECT * FROM cookies\") with open('cookies.csv', 'a', newline='') as csvfile: fieldnames = ['host_key', 'name', 'value', 'path', 'expires_utc', 'is_secure', 'is_httponly'] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) if csvfile.tell() == 0: writer.writeheader() for row in cursor.fetchall(): decrypted_value = decrypt_password(row[\"encrypted_value\"], master_key) writer.writerow({ 'host_key': row[\"host_key\"], 'name': row[\"name\"], 'value': decrypted_value, 'path': row[\"path\"], 'expires_utc': row[\"expires_utc\"], 'is_secure': row[\"is_secure\"], 'is_httponly': row[\"is_httponly\"] }) print(\"Finished CSV\") Ce programme va faire tout ce que j'ai expliqu\u00e9 et va ensuite stocker les r\u00e9sultats dans un CSV pour qu'il soit facile d'y acc\u00e8der depuis le C#. Alors oui cela pose certaines questions de s\u00e9curit\u00e9. Car en effet je prend tous les cookies, les d\u00e9code et les stocke. Ce qui veut dire que je pourrais tout \u00e0 fait envoyer ces donn\u00e9es quelque part, par exemple un compte Netflix, et me rincer. Si je devais rendre le projet ouvert au public je pense qu'il faudra que cela soit mentionn\u00e9 clairement et que le projet soit open source pour que les utilisateurs puissent verifier que je ne fais pas ca. Maintenant de l'autre c\u00f4t\u00e9 j'ai juste \u00e0 lire le CSV et le tour est jou\u00e9 ! (Trouver cette solution m'a pris une semaine de vacances \u00e0 l'\u00e9poque) Bon j'ai r\u00e9ussi \u00e0 faire le programme se connecter et naviguer etc.. Par contre quelque chose que j'ai voulu ajouter et qui m'a pris pas mal de temps c'est de faire en sorte de pouvoir selectionner la qualit\u00e9. Pour changer la qualit\u00e9 du feed il faut cliquer sur settings et ensuite prendre le menu deroulant et selectioner 1080p. Le soucis c'est le que la value du select est jamais la m\u00eame. Elle commence toujours pas \"1080_\" mais ensuite ca peut \u00eatre \"1080_45930285\" ou \"1080_56801\" la suite est apparemment random. J'ai donc du utiliser ce code pour le selectioner quand m\u00eame : IWebElement settingsButton = driver.FindElement(By.ClassName(\"bmpui-ui-settingstogglebutton\")); settingsButton.Click(); IWebElement selectElement = driver.FindElement(By.ClassName(\"bmpui-ui-videoqualityselectbox\")); SelectElement select = new SelectElement(selectElement); IWebElement selectOption = selectElement.FindElement(By.CssSelector(\"option[value^='1080_']\")); selectOption.Click(); Sauf que pour que cela marche je dois avant cliquer sur le bouton des settings le probl\u00e8me c'est qu'il est invisible alors on doit le faire apparaitre. J'ai tent\u00e9 de le faire aparaitre en bougeant la souris, en cliquant \u00e0 un endroit pr\u00e9cis, impossible de le faire marcher correctement. Puis j'ai eu l'id\u00e9e de mettre pause en envoyant un appui sur la touche Espace et ca a permit de d\u00e9couvrir le bouton et permettre qu'on clique dessus. Ca peut paraitre tout b\u00eate mais rien que ca, ca m'a pris un temps consid\u00e9rable. Bon pour ce qui est du timecode de la vid\u00e9o. Je pense qu'il serait trop complexe de faire en sorte que selenium change le slider de progression de la vid\u00e9o. Alors j'ai fait quelques tests et apparemment, si on quitte la F1TV sur un timecode de la vid\u00e9o que on donne au programme, comme il r\u00e9cup\u00e8re tous les cookies de la F1TV il commencera de la. Donc si on veut utiliser le programme avec des Grand Prix ayant d\u00e9ja eu lieu, on peut le faire, seulement il faudra juste au pr\u00e9alable avoir choisit le bon timecode dans le page de la F1TV avant de le lancer. Ce qui est int\u00e9ressant c'est que la page de la F1TV ressemble \u00e0 ca au d\u00e9part : \"Empty F1TV\" Je pense qu'une bonne id\u00e9e serait de dire au programme que c'est la grille de d\u00e9part et ensuite d\u00e8s qu'il d\u00e9tecte un secteur il sait que la course a commenc\u00e9.","title":"Vacances"},{"location":"jdb.html#lundi-24-avril-2023","text":"Aujourd'hui c'est jour de documentation. J'ai pas mal travaill\u00e9 pendant les vacances mais je n'ai pas encore pu faire de vraie documentation correcte du fonctionnement. Du coup je vais m'en charger aujourd'hui et peut-\u00eatre un peu demain. Ok normalement je ne devrais faire que de la documentation mais je ne peux pas passer \u00e0 cot\u00e9 de ca... Le probl\u00e8me que j'ai avec les pneus ou parfois il d\u00e9tecte un H au lieu d'un '11' et ce genre de choses c'est \u00e0 cause de ma methode \"RemoveBG\" Qui va retirer tous les pixels plus sombres que le background. Sauf que cela va aussi retirer des pixels dans le chiffre lui m\u00eame et qui va donc defigurer les 11 : \"diformed 11\" \"diformed 11\" J'ai r\u00e9ussi \u00e0 les changer en : \"less diformed\" \"less diformed\" Mais au final cela n'a pas augment\u00e9 la pr\u00e9cision de la reconnaissance. Je pense que je vais donc devoir encore changer. Je pense que une bonne facon de trouver serait dabord de trouver la couleur du pneu. Et si il n'y a pas assez de couleur alors c'est que le pneu contient une lettre. Le but est d'arr\u00eater de chercher des lettres ou des chiffres. Comme ca les 11 arr\u00eateront d'\u00eatre pris pour des 'H' En fait on peut faire encore plus simple que ca. On peut simplement regarder la couleur dominante et determiner le pneu. En effet m\u00eame si il y a une lettre sur fond noir pour d\u00e9crire le pneu, mon methode de r\u00e9cup\u00e8ration de la couleur dominante ommet les pixels trop noirs alors il est quand m\u00eame possible de determiner le type de pneus. Et tout simplement si il n'arrive pas \u00e0 lire le chiffre c'est que c'est une lettre et que donc on est \u00e0 0 tours. Cela marche plut\u00f4t bien et cela simplifie pas mal le processing. Voila, la je vais me remettre \u00e0 la documentation sinon je vais encore prendre du retard.","title":"Lundi 24 Avril 2023"},{"location":"jdb.html#mardi-25-avril-2023","text":"Encore une fois j'ai pris du temps de doc pour changer des choses sur la partie OCR. Mais en m\u00eame temps en documentant je vois des choses que j'ai soit mal fait soit que je pourrais faire mieux en changeant tr\u00e8s peu de choses. J'\u00e9sp\u00e8re que les changement que j'ai fait vont aider au moins \u00e0 la coh\u00e9rence du code et un peu pour les performances. Il semble que dans les conditions que j'ai test\u00e9 le nombre de tour soit plut\u00f4t fiable mais je pense que je devrai faire un peu de travail en aval dans la r\u00e9cup\u00e9ration de ces donn\u00e9es car je sens que cela va poser probl\u00e8me quelques fois. Je pense que en utilisant bien l'historique on peut potentiellement se passer de l'utilisation de ce chiffre pas toujours compl\u00eatement fiable. Mais sinon aujourd'hui c'est encore une fois un gros jour de doc. J'essaie d'expliquer les diff\u00e9rents proc\u00e9d\u00e9s avant de les oublier. J'essaie aussi de donner un maximum d'exemples sous formes de photos interm\u00e9diaires mais ca me prend pas mal de temps car il faut que j'ajoute un peu partout dans le code des lignes pour sortir des images interm\u00e9diaires. En plus de la documentation je me suis aussi beaucoup occup\u00e9 de nettoyer mon code et je suis assez content par ce que m\u00eame en ayant du rajouter des couches de complexit\u00e9 pour mieux reconnaitres les temps au tour j'arrive \u00e0 un temps de processing parfois en dessous des 2 secondes ce que je trouve honorable. Quand j'aurai finit de nettoyer tous mes fichiers je ferai une release sur gitea et ce sera la version que j'utiliserai quand je voudrai faire un merge avec les autres parties du projet. J'ai beaucoup beaucoup boss\u00e9 aujourd'hui et je sui bien mort. Faire autant de documentation et de nettoyage de code c'est pas forc\u00e9ment bon pour le cerveau je crois. J'ai besoin d'une sieste. Demain je pense que je vais commencer \u00e0 avancer sur la partie r\u00e9cup\u00e8ration des images. Je sais que la je fais un peu passer les tests \u00e0 la trappe mais d\u00e9ja j'en ai fait tout le long du d\u00e9veloppement de OCR_DECODE et il faut vraiment que j'avance, quitte \u00e0 revenir dessus quand j'aurai merge les deux projets ensemble.","title":"Mardi 25 Avril 2023"},{"location":"jdb.html#26-avril-2023","text":"Aujourd'hui je vais devoir m'occuper de la partie r\u00e9cup\u00e9ration des images. J'ai d\u00e9ja eu l'occasion d'avancer sur ce projet pendant mopn poc et mes vacances. Donc la le but ca va \u00eatre de voir ce qui manque comme v\u00e9ritables features et ensuite je vais pouvoir m'occuper de la vue et de son int\u00e9gration avec le d\u00e9codage. Ok donc maintenant que j'au un programme qui arrive \u00e0 prendre des images depuis la F1TV correctement et en bonne r\u00e9solution. Je pense qu'il est temps de passer \u00e0 l'impl\u00e9mentation de la Forme que ca va prendre. C'est important de se poser au moins cinq minutes la question de comment je pr\u00e9vois de faire car m\u00eame si ca n'est pas la version finale, cette derni\u00e8re prendra tr\u00e8s fort inspiration du desing que je vais faire. Dans cette form j'aurais besoin de : Pouvoir selectionner un Grand Prix en ins\u00e8rant l'URL du feed. Pouvoir lancer la calibration si besoin Indiquer le titre et la date du Grand Prix Indiquer si le Grand Prix vient de commencer ou si il y a d\u00e9ja un certain nombre de tours lanc\u00e9s. Et c'est \u00e0 peu pr\u00e8s tout en fait... J'ai tellement pouss\u00e9 pour avoir un programme qui fait tout tout seul que il ne me faut pas grand chose de plus. Je pense que ce qui serait pas mal ca serait du coup d'utiliser ce temps pour bien impl\u00e9menter la calibration qui elle aura besoin d'une UI un peu plus bal\u00e8ze. On pourrait m\u00eame imaginer que la calibration fasse partie int\u00e9grante des settings... Ca serait peut-\u00eatre bien que quand l'application se lance on se retrouve sur la page principale d'affichage de donn\u00e9es et qu'on puisse simplement cliquer sur la page options qui contient la page calibration et qui permet de rentrer les infos du Grand Prix. Je pense que je vais faire ca. Voici l'interface que j'ai d\u00e9velopp\u00e9e pour regrouper tout ca : \"Screen\" La police le style le placement et les couleurs ne sont pas d\u00e9finitfs, cependant je pense que c'est un bon d\u00e9but. Le but maintenant va \u00eatre de permettre de faire fonctionner la calibration et la r\u00e9cup\u00e8ration d'images. Si j'arrive \u00e0 faire fonctionner ces deux choses sur un m\u00eame projet avant la fin de la semaine cela serait super ! Bon J'ai pu avancer sur l'int\u00e8gration de Selenium mais cela prend un peu de temps car je veux impl\u00e9metner un moyen de pouvoir prendre une Screenshot \u00e0 nimporte quel moment et pas juste en boucle. Demain je finis de faire fonctionner ca et ensuite je commence le cablage du reste.","title":"26 Avril 2023"},{"location":"jdb.html#jeudi-27-avril-2023","text":"C'est assez dur de faire l'importation car il y a des petites diff\u00e9rences qui obligent \u00e0 presque tout r\u00e9\u00e9crire. En fait le programme de calibration avait d\u00e9ja impl\u00e9ment\u00e9 la fonction de Windows et de Zones mais il fonctionnait juste assez diff\u00e9remment pour qu'il faille tout refaire. La je suis en train de perdre \u00e9norm\u00e9ment de temps \u00e0 cause d'un soucis de coordonn\u00e9es. J'ai repris le code de la calibration pour detecter ou l'utilisateur a cliqu\u00e9 pour cr\u00e9er les zones. Cependant, je n'arrive pas \u00e0 le faire fonctionner correctement. La zone est tout le temps d\u00e9cal\u00e9e en haut et en bas mais pas de la m\u00eame facon. En haut, la valeur Y est trop grande alors que en bas la valeur Y est trop petite... Je ne comprends pas bien pourquoi. Si c'\u00e9tait un simple d\u00e9calage cela ne serait pas compliqu\u00e9 \u00e0 g\u00e8rer mais la... J'ai un soucis \u00e9galement avec la r\u00e9solution des screenshots que je r\u00e9cup\u00e8re en full Headless. Voici un exemple de r\u00e9solution que j'arrive \u00e0 r\u00e9cup\u00e8rer sans le headless : \"High Res\" \"Low Res\" Il y a clairement un soucis et le probl\u00e8me c'est que avec une r\u00e9solution pareille, impossible de faire une reconnaissance correcte. BON J?EN PEUX PLUS LA. Ca fait des heures que je bosse sur ce probl\u00e8me d\u00e9bile et impossible de trouver une solution. J'ai essay\u00e9 cinq facons de forcer le browser headless a prendre une plus haute r\u00e9solution aucune ne fonctionne je ne comprends pas. A chaque fois que je me retrouve avec une r\u00e9solution de 1366 x 768 Ou une variante de basse r\u00e9solution du style. J'en peux plus je ne trouve aucune r\u00e9ponse sur internet ni m\u00eame avec chatGPT. Super... La seule chose que j'ai pu faire qui change quelque chose fait que les images font maintenant du 926x517... j'ai un peu envie de commentre un crime de guerre au plus vite.","title":"Jeudi 27 Avril 2023"},{"location":"jdb.html#vendredi-28-avril-2023","text":"Une des solutions que je n'ai pas encore essay\u00e9 est de changer ma version de GeckoDriver. Sauf que ca m'oblige \u00e0 changer les versions de mes libraries ce qui est tr\u00e8s p\u00e9nible, je vais continuer le debugging dans le projet Selenium_clean. Il faut savoir que la librairie de Selenium que j'utilise est bloqu\u00e9e en 0.27 ce qui fait que je ne peux utiliser qu'une version obsol\u00e8te du Gecko Driver. J'ai tent\u00e9 de changer vers une version en 64 bits du GeckoDriver 0.27 mais pareil, je me retrouve toujours avec des images de M. J'essaie toutes les solutions que je trouve sur internet aucune ne convient c'est infernal. J'essaie de changer la r\u00e9solution DPI, j'essaie de changer les param\u00eatres par d\u00e9faut des player de Firefox, j'essaie de changer la r\u00e9solution pendant et au d\u00e9but de l'execution IMPOSSIBLE DE FAIRE MARCHER CETTE MERDE C'EST PAS POSSIBLE !!! J'ai essay\u00e9 avec chrome mais je ne peux pas l'utiliser car les DRM m'emp\u00eacheront de prendre des screenshot du flux vid\u00e9o. J'ai essay\u00e9 de faire tourner avec edge mais edge ne peut pas tourner en headless. JE VAIS DEVENIR FOUF FPWQOVMQEKOVNVIBDBJDAIVOBI. ET MAINTENANT JE N'ARRIVE PLUS A FAIRE DE PROJET AVEC SELENIUM VOIWQNV(UEWQBVU)WEQN=OEJNIVIUWVBWUEV ON CHERCHE A ME FAIRE PETER UN PLOMB C'EST PAS POSSIBLE GIWEGUWEQN VOICI UN EXEMPLE DU CODE QUE JE DEMANDE A UN NOUVEAU PROJET AVEC EXACTEMENT LES MEMES LIBRARIES INSTALLEES : // Create a new FirefoxDriver instance IWebDriver driver = new FirefoxDriver(); // Navigate to the specified URL driver.Navigate().GoToUrl(\"https://www.example.com\"); // Do something with the driver (e.g., find elements or take screenshots) // Quit the driver driver.Quit(); Je ne demande que ca ET MEME CA CA NE VEUT PAS FONCTIONNER VOIWENB)IWUQENV Oui je suis un peu \u00e9nerv\u00e9 ca se voit? A bon? Et maintenant NUGGET ne fonctionne plus... j'en peux plus la. Je ne peux plus t\u00e9l\u00e9charger de librairie sur aucun de mes projets... J'ai tent\u00e9 de supprimer le fichier de config et red\u00e9marrer Visual Studio mais cela ne fait rien. J'ai aussi tent\u00e9 de faire un 'nugget restore' toujours rien. Bon apparemment je ne suis pas le seul qui ne peut pas acc\u00e8der \u00e0 Nuget donc bon c'est pas juste chez moi qu'il y a un soucis. Mais m\u00eame en mettant ma 4G pour me connecter, je n'arrive pas \u00e0 acc\u00e8der \u00e0 certains sites y compris Nuget et je ne peux pas download de librairies... Je ne comprends pas ce qui se passe et du coup je ne peux juste pas bosser... J'ai red\u00e9marr\u00e9 trois fois mon pc et visual studio, j'ai essay\u00e9 de changer mes settings DNS etc... impossible de bosser. Je crois que je n'aurais pas du me reveiller aujourd'hui. Bon je vais tenter d'avancer sur mon poster en attendant que le r\u00e9seau soit en meilleur \u00e9tat.","title":"Vendredi 28 Avril 2023"},{"location":"jdb.html#lundi-1-mai-2023","text":"Bon je bosse depuis chez moi donc j'esp\u00e8re que Nuget va mieux fonctionner. Apr\u00e8s un weekend \u00e0 r\u00e9fl\u00e9chir au sujet de cette resolution je me suis dit deux choses. La seule personne sur internet que j'ai vu avoir le meme soucis avait une r\u00e9solution de 1920x1200 comme moi. Cela veut donc s\u00fbrement dire que le soucis vient de cette r\u00e9solution de laptop comme moi. Si vraiment je n'arrive pas dans un premier temps \u00e0 faire fonctionner le Headless correctement, je peux toujours laisser la page de c\u00f4t\u00e9 et m'occuper du reste du programme. Certes ca serait vraiment infernal d'avoir \u00e0 garder une page chrome ouvert en tous temps et en plus elle doit \u00eatre en plein \u00e9cran mais bon... Si il n'y a vraiment pas d'autres solutions malheureusement je serai bien oblig\u00e9. BON ! JE N'ARRIVE MEME PLUS A FAIRE UN PROJET QUI UTILISE SELENIUM ET QUI MARCHE JE VAIS FAIRE BR\u00dbLER GENEVE. C'est pas possible serieux, je ne comprends pas j'essaie tout ce que je trouve et impossible de juste lancer firefox c'est du grand nimporte quoi. Je prend les m\u00eame putain de librairies que sur les autres projets les m\u00eames versions, je prend le m\u00eame exact code. Sur le nouveau projet impossible de le faire fonctionner. Je commence \u00e0 croire que on essaie de me faire p\u00eater un cable. Du coup dans un \u00e9lan de d\u00e9sespoir je vais tenter de passer sur une autre librairie qui avec un peu de chance marche et en plus me permettrais de prendre des foutues screenshot dans le bon format. Les deux seules librairies qui pourraient potentiellement faire l'affaire sont les librairies : PhantomJS CefSharp Je vais les tester et simplement prier pour qu'elles fonctionnent et que je puisse faire ce que je veux avec. Alors pour le moment avec CEFSharp j'arrive \u00e0 lancer une instance de chrome et prendre une screenshot avec ce code : CefSettings settings = new CefSettings(); settings.CachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), \"CefSharp\\\\Cache\"); // Set cache path settings.LogSeverity = LogSeverity.Disable; // Disable logging Cef.Initialize(settings); // Initialize CEF using (var browser = new ChromiumWebBrowser(\"www.google.com\", new BrowserSettings())) // Launch Chromium in off-screen mode { browser.Load(\"https://www.example.com\"); // Navigate to the test URL browser.Size = new Size(1920, 1080); // Set the browser size to 1920x1080 browser.ScreenshotAsync().ContinueWith(task => { var bitmap = task.Result; bitmap.Save(\"screenshot.png\", System.Drawing.Imaging.ImageFormat.Png); // Take a screenshot and save it as a PNG file }).Wait(); } Cef.Shutdown(); // Shutdown CEF Avec ca il faut ces using : using System; using System.Drawing; using System.IO; using CefSharp; using CefSharp.OffScreen; C'est assez prometteur m\u00eame si il faut encore beaucoup pour remplacer selenium. Ah bah lol en fait non on peut pas utiliser cette librarie pour faire tourner firefox... J'EN AI MARRE J'AVAIS CHERCHE PRECISEMENT UNE LIB QUI MARCHE AVEC FIREFOX Et phantomJS non plus ne fonctionne pas avec firefox... J'en ai marre. Donc je vais plut\u00f4t partir sur la librairie GeckoFX qui semble pouvoir contr\u00f4ler une instance de firefox. Mais j'avais justement pris un putain de projet C# et pas JS pour ne pas me taper ces probl\u00e8mes de librairies... Et si cette option ne fonctionne pas mon dernier espoir sera de directement int\u00e9ragir avec le geckodriver.exe et la ca risque de pas \u00eatre dr\u00f4le. JE NE COMPRENDS RIEN !!!!! Ca n'a aucun sens la doc est inexistante le seul lien qui pourrait amener sur une doc envoie sur la page principale de bitbucket. Tous les exemples de code que je trouve ne fonctionnent pas. Je n'arrive \u00e0 rien je commence \u00e0 devenir fou. Tout ce travail pour rien c'est pas possible. M\u00eame en essayant directement d'int\u00e9ragir avec le process geckodriver.exe je ne peux pas arriver \u00e0 mes fins. J'arrive \u00e0 lancer le service et tout, mais je n'arrive pas \u00e0 vraiment contr\u00f4ler ce qu'il se passe donc impossible de venir prendre des screenshot. Je ne sais tout simplement pas quoi faire ... Je suis bloqu\u00e9. Je me suis cass\u00e9 la t\u00eate \u00e0 faire un truc qui marchait bien avec selenium et tout. Mais maintenant plus rien ne fonctionne du jour au lendemain et il n'y a simplement aucune alternative. Je vais essayer de changer directement le projet Selenium_Clean mais bon ca va pas \u00eatre dr\u00f4le. Ok alors j'ai tout repris depuis le d\u00e9but et je crois que j'ai enfin une solution. Pour la trouver j'ai re-essay\u00e9 toutes les techniques que j'avais tent\u00e9 avant mais dans l'ordre et en les isolant \u00e0 chaque fois. Cela inclus : Tenter de changer la densit\u00e9 de pixels. En effet je me suis dit que comme la r\u00e9solution \u00e9tait plus basse le soucis \u00e9tait que le virtual screen avait simplement une DPI r\u00e9duite. profile.SetPreference(\"layout.css.devPixelsPerPx\", \"2.0\"); J'ai aussi tent\u00e9 de r\u00e9duire \u00e0 un seule le nombre de process de Firefox. J'ai pu lire sur internet que parfois cela pouvait influer sur les performances du renderer. profile.SetPreference(\"dom.ipc.processCount\", 1); Ensuite j'ai tent\u00e9 tout b\u00eatement de rajouter dans la liste des arguments la taille voulue de l'\u00e9cran. options.AddArgument(\"--window-size=1920,1080\"); Mais comme cela ne foncionnait pas, je me suis rabattu sur un script JS pour tenter de forcer la fen\u00eatre \u00e0 \u00eatre plus grande. js.ExecuteScript(\"window.resizeTo(1920, 1080);\"); Comme cela n'a pas march\u00e9 j'ai pu lire que cela pouvait \u00eatre la taille int\u00e9rieure qui devait \u00eatre chang\u00e9e js.ExecuteScript(\"window.innerWidth = 1920; window.innerHeight = 1080;\"); Encore une fois sans succ\u00e8s. J'ai ensuite tent\u00e9 d'utiliser trois autres versions du GeckoDriver, 0.27,0.26,0.25 et aucune ne m'aidait. Mais en fait la seule chose qui a chang\u00e9 quoi que ce soit \u00e9tait la technique suivante : Changer la window size en utilisant : options.AddArgument(\"--width=1920\"); options.AddArgument(\"--height=1200\"); Ca ne marchait pas car j'utilisais une autre methode pour resize en m\u00eame temps, qui elle ne marchait pas mais qui emp\u00eachait celle la de marcher. Ensuite le soucis que j'avais c'est que en mettant 1920-1080 je me retrouvais avec 1920-998 ou un truc du genre ce qui n'\u00e9tait pas normal alors je me disais que cette technique ne marchait pas non plus et je l'ai pass\u00e9e. Alors tout n'est pas encore gagn\u00e9, il faut que j'arrive \u00e0 impl\u00e9menter ca dans un plus gros projet et que la vid\u00e9o puisse \u00eatre prise seule. Demain je m'occupe de ca.","title":"Lundi 1 Mai 2023"},{"location":"jdb.html#mardi-2-mai-2023","text":"Bon aujourd'hui je change le programme principal. Le soucis que j'ai c'est que en ajoutant ce syst\u00e8me de resize, maintenant la page fait 100x100 et est grise. Il doit y avoir une technique que j'ai oubli\u00e9 de retirer ou un comportement un peu bizarrre. Bon clairement je ne sais pas QUI DECIDE DE ME POURRIR LA VIE mais il est fort. J'ai t\u00e9l\u00e9charger EXACTEMENT les m\u00eames librairies que sur mon autre projet et j'utilise l'EXACT m\u00eame geckodriver.exe mais dans le projet principal impossible de lui faire chier une image m\u00eame avec l'EXACT m\u00eame code. POURQUOI VOUS ME FAITES CA????= La je ne comprend vraiment pas ce qui peut se passer pour que rien ne fonctionne alors que tout est pareil. JE VIENS DE TOUT VERIFIER TOUT EST PAREIL JE NE COMPRENDS PAS. Bon apr\u00e8s avoir supprim\u00e9 l'int\u00e9gralit\u00e9 de ma classe Emulator cela semble marcher un peu mieux. Je ne vais pas m'\u00e9tendre sur la castrophe niveau temps que cela repr\u00e9sente. Si au moins j'arrive \u00e0 faire fonctionner quelque chose je suis content. Maintenant j'ai un soucis un peu sp\u00e9cial. Depuis que j'ai chang\u00e9 la r\u00e9solution, il semble que le programme aie du mal \u00e0 cliquer sur l'icone de settings. En prenant des screenshots du moment ou l'erreur apparait, j'ai pu me rendre compte que en fait le stream est toujours en train de charger et c'est pour ca que on arrive pas \u00e0 trouver le bouton : \"ERROR 105\" \"ERROR 105\" Je pense que je n'ai le soucis que maintenant car le flux en 1080p se lance moins vite. Je vais essayer de voir si je peux detecter un \u00e9l\u00e9ment d'HTML qui correspond au loading comme ca je peux attendre qu'il disparaisse. Sinon je peux aussi juste essayer de trouver le bouton en boucle pendant une dixaine de secondes. Bon la j'essaie pendant genre plus de 50 secondes et ca ne marche toujours pas. Il semblerait que au final le probl\u00e8me vienne du GP d'azerbidjan. En effet, quand je teste un autre Grand Prix tout va bien. ET MERDE ! J'ai r\u00e9ussi \u00e0 avoir des images en 1080P mais d\u00e9s que je passe l'image en plein \u00e9cran c'est de nouveau du 1366X768 Avant de mettre en plein \u00e9cran: \"Before fullscreen\" Apr\u00e8s: \"After fullscreen\" On peut voir sur l'image que l'option 1080P est effectivement bien selectionn\u00e9e mais il doit y avoir un param\u00e8tre de Firefox qui s'occupe de la r\u00e9solution d'un player vid\u00e9o. Il va juste falloir trouver ce param\u00eatre... J'ai essay\u00e9 d'utiliser : Driver.Manage().Window.Size = new System.Drawing.Size(windowWidth, windowHeight); Sans succ\u00e8s. options.AddArgument(\"--start-maximized\"); Pareil Driver.Manage().Window.Maximize(); Toujours rien profile.SetPreference(\"full-screen-api.ignore-widgets\", true); Nada profile.SetPreference(\"media.hardware-video-decoding.enabled\", true); Toujours pas J'ai vraiment cru que j'avais trouv\u00e9 la solution en trouvant cette commande profile.SetPreference(\"full-screen-api.enabled\", true); Mais non toujours pas... Je commence \u00e0 perdre patience. C'EST BON. Apr\u00e8s litt\u00e9rallement 3h de debugging avec M.Bonvin (Que je remercie IMMENSEMENT) on a r\u00e9ussi \u00e0 trouver au fin fond d'un thread github que la valeur \u00e9tait hard cod\u00e9e dans les variables d'environnement et que donc quoi que je fasse je n'aurais pas pu le changer. En fait la seul moyen de tout r\u00e8gler a \u00e9t\u00e9 de changer les variables d'environnement de ma machine: MOZ_HEADLESS_WIDTH et MOZ_HEADLESS_HEIGHT . Et ce qu'il y a de bien c'est que maintenant je peux mettre de la 4K et cela permet de faire un meilleur upscaling.","title":"Mardi 2 Mai 2023"},{"location":"jdb.html#recrutement-payerne-mai-2023","text":"J'ai du faire mon recrutement \u00e0 Payerne Mercredi et Jeudi. Si vous \u00eates curieux je peux vous dire que comme il n'y avait presque plus de places cet \u00e9t\u00e9 je ferai Canonnier Lance mines. C'\u00e9tait assez frustrant d'avoir perdu deux jours de travail mais on va faire avec.","title":"Recrutement Payerne Mai 2023"},{"location":"jdb.html#vendredi-5-mai-2023","text":"Bon malgr\u00e9s les courbatures il faut que je me mette au boulot un peu serieusement par ce que sinon ca va \u00eatre compliqu\u00e9 de rattraper mon retard. La derni\u00e8re fois si je me souviens bien j'avais r\u00e9ussi \u00e0 trouver un moyen de prendres des images en bonne r\u00e9solution. Il faut maintenant que je commence \u00e0 faire fonctionner la calibration et ce qui serait bien ca serait que je commence \u00e0 ajouter la partie OCR au projet. Il faut que je me d\u00e9p\u00eache car Lundi je dois m'occuper du Poster. OK j'ai compris le soucis que j'avais quand j'essayais de faire la calibration. J'avais mis l'image en ZOOM ce qui fait que si la hauteur n'\u00e9tait pas la bonne, l'image \u00e9tait recentr\u00e9e ce qui fait que cela faussait totalement les r\u00e9sultats. Quand on fait en sorte que l'image prenne toute la place, les coordonn\u00e9es sont prises correctement. Voici un exemple d'ou en est la partie calibration. \"Exemple settings UI\" Normalement il me suffit d'impl\u00e9menter les windows, et on devrait relativement facilement ajouter les pilotes. Et voila. J'ai pu impl\u00e9menter les windows et les pilotes. Et je peux aussi exporter des presets et les loader. Bon le loading est un peu beugg\u00e9 au niveau de l'affichage mais il semble qu'il fonctionne bien quand je save les images. Lundi je m'occupe du poster etc.. mais je pense que la suite va \u00eatre l'impl\u00e9mentation de l'OCR.","title":"Vendredi 5 Mai 2023"},{"location":"jdb.html#lundi-8-mai-2023","text":"Aujourd'hui c'est journ\u00e9e Poster. Je pense que je ne vais pas finir la journ\u00e9e content car les limitations sont un peu trop pr\u00e9sentes. J'ai fait une version que Garcia pourrait accepter, c'est \u00e0 dire en noir et blanc et avec un tout petit peu plus de d\u00e9tail. \"Poster V3\" Le truc c'est que en blanc je trouve que ca ne marche pas super. Et le concept d'avoir trois parties au projet qui se posent autour d'un circuit c'est peut-\u00eatre pas la meilleure id\u00e9e. Je me suis dit que la bonne id\u00e9e serait peut-\u00eatre de prendre un autre circuit pour qu'il y aie bien trois parties : \"Poster V4\" Clairement ce poster doit faire partie des pires. C'est pas clair et ca part dans tous les sens. Je vais essayer avec un autre layout de circuit. \"Poster V5\" Je me suis ensuite dit que le circuit n'\u00e9tait peut \u00eatre tout simplement pas une bonne id\u00e9e. J'ai donc essay\u00e9 de faire quelque chose de plus classique avec juste un peu de background pour qu'on puisse \u00e9viter le soucis de la page blanche derri\u00e8re : \"Poster V6\" Puis je me suis dit que finalement le circuit me manquait. Alors j'ai d\u00e9cid\u00e9 de combiner le background et le circuit ainsi que simplifier l\u00e9g\u00e8rement les diagrammes en retouchant un peu tout le reste on pouvait arriver \u00e0 quelque chose de sympatique : \"Poster V7\" Je ne suis pas content \u00e0 100% mais bon je pense que je vais m'en satisfaire. Pour donner une id\u00e9e de la gal\u00e8re que c'est de cr\u00e9er un poster, voici ce \u00e0 quoi ressemble mon espace de travail Figma : \"Bordel Figma\" Je ne suis pas un graphiste et ca se voit '^^. Je pense que comme il me reste un peu de temps aujourd'hui, je vais faire un peu de documentation de la partie r\u00e9cup\u00e8ration d'images. En effet, je pense que je n'aurai plus besoin de changer grand chose \u00e0 ce niveau. Mais je ne ferai pas la partie analyse fonctionnelle car l'interface n'est clairement pas termin\u00e9e. En fait j'avais oubli\u00e9 mais j'ai eu un rendez vous m\u00e9dical du coup je n'ai pas eu trop le temps de faire la doc que je voulais. Mais au moins je pense avoir finit mon travail sur le poster et le abstract en Anglais qui sont les deux gros livrables \u00e0 venir.","title":"Lundi 8 Mai 2023"},{"location":"jdb.html#mardi-9-mai-2023","text":"Bon je viens de me rendre compte que apparemment on doit rendre l'abstract anglais, le Poster, ET LE PROJET. Je pense que mes deux jours \u00e0 l'arm\u00e9e m'ont fait perdre un peu la notion du temps car j'avais l'impression que l'evaluation interm\u00e9diaire 1 \u00e9tait il y a genre moins d'une semaine. Donc aujourd'hui je ne vais pas trop avancer sur le code et vraiment me focus sur la documentation de la r\u00e9cup\u00e8ration d'images. Je pense que je vais aussi ajouter la partie calibration \u00e0 la documentation. Je pense que c'est important que je prenne le temps maintenant car sinon le prof aura l'impression que ca n'a pas trop avanc\u00e9 depuis la derni\u00e8re fois. Et puis je pense que la partie calibration et r\u00e9cup\u00e8ration d'images ne va pas trop changer et la partie calibration encore moins. La partie anglaise je fais la revoir un peu mais je l'avais d\u00e9ja faite pendant les premiers jours alors ca devrait aller. Pour le rendu il nous \u00e9tait demand\u00e9 de fournir un fichier PDF avec tout dedans avec une table des mati\u00e8res notre code source etc... Pour ce faire j'ai du changer le mkdocs.yml et installer des packages. Voici les changements :: site_name: Documentation Track Trends site_author: Rohmer Maxime copyright: \u00a9CFPTI Tech2 theme: name: material palette: # Palette toggle for light mode - media: \"(prefers-color-scheme: light)\" scheme: default toggle: icon: material/brightness-7 name: Switch to dark mode # Palette toggle for dark mode - media: \"(prefers-color-scheme: dark)\" scheme: slate toggle: icon: material/brightness-4 name: Switch to light mode markdown_extensions: - attr_list - md_in_html - pymdownx.highlight plugins: - glightbox - search - img2fig - with-pdf: cover_subtitle: Vroum Vroum enabled_if_env: ENABLE_PDF_EXPORT - annexes-integration: annexes: # Required (at least 1) - ConfigurationTool.cs: Code/ConfigurationTool.cs # An path to an annex with its title - DriverGapToLeaderWindow.cs: Code/DriverGapToLeaderWindow.cs # An path to an annex with its title - DriverPositionWindow.cs: Code/DriverPositionWindow.cs # An path to an annex with its title - F1TVEmulator.cs: Code/F1TVEmulator.cs # An path to an annex with its title - Program.cs: Code/Program.cs # An path to an annex with its title - Window.cs: Code/Window.cs # An path to an annex with its title - DriverData.cs: Code/DriverData.cs # An path to an annex with its title - DriverLapTimeWindow.cs: Code/DriverLapTimeWindow.cs # An path to an annex with its title - DriverSectorWindow.cs: Code/DriverSectorWindow.cs # An path to an annex with its title - Form1.cs: Code/Form1.cs # An path to an annex with its title - Reader.cs: Code/Reader.cs # An path to an annex with its title - Zone.cs: Code/Zone.cs # An path to an annex with its title - DriverDrsWindow.cs: Code/DriverDrsWindow.cs # An path to an annex with its title - DriverNameWindow.cs: Code/DriverNameWindow.cs # An path to an annex with its title - DriverTyresWindow.cs: Code/DriverTyresWindow.cs # An path to an annex with its title - OcrImage.cs: Code/OcrImage.cs # An path to an annex with its title - Settings.cs: Code/Settings.cs # An path to an annex with its title - recoverCookiesCSV.py: Code/recoverCookiesCSV.py # An path to an annex with its title Je remercie Monsieur Briard le sultan officiel de Mkdocs de la classe de m'avoir aid\u00e9 pour cette partie et avoir cr\u00e9\u00e9 un plugin qui me permet de mettre mon code source directement dans le pdf. Bon au final j'ai quand m\u00eame chang\u00e9 mon poster \"Poster V8\" Mais je suis trop attach\u00e9 \u00e0 l'ancien concept alors je vais plut\u00f4t utiliser ca : \"Poster V9\"","title":"Mardi 9 Mai 2023"},{"location":"Code/ConfigurationTool.html","text":"ConfigurationTool.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : ConfigurationTool.cs /// Brief : Class that contains all the methods needed to create a config file for the OCR /// Version : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using Tesseract; using System.IO; namespace Test_Merge { public class ConfigurationTool { public Zone MainZone; public const int NUMBER_OF_DRIVERS = 20; public const int NUMBER_OF_ZONES = 9; public const string CONFIGS_FOLDER_NAME = \"./Presets/\"; public ConfigurationTool(Bitmap fullImage, Rectangle mainZoneDimensions) { MainZone = new Zone(fullImage, mainZoneDimensions,\"Main\"); AutoCalibrate(); } public void ResetMainZone() { MainZone.ResetZones(); } public void ResetWindows() { MainZone.ResetWindows(); } public void SaveToJson(List drivers, string configName) { string JSON = \"\"; JSON += \"{\" + Environment.NewLine; JSON += MainZone.ToJSON() + \",\" + Environment.NewLine; JSON += \"\\\"Drivers\\\":[\" + Environment.NewLine; for (int i = 0; i < drivers.Count; i++) { JSON += \"\\\"\" + drivers[i] + \"\\\"\"; if (i < drivers.Count - 1) JSON += \",\"; JSON += Environment.NewLine; } JSON += \"]\" + Environment.NewLine; JSON += \"}\"; if (!Directory.Exists(CONFIGS_FOLDER_NAME)) Directory.CreateDirectory(CONFIGS_FOLDER_NAME); string path = CONFIGS_FOLDER_NAME + configName; if (File.Exists(path + \".json\")) { //We need to create a new name int count = 2; while (File.Exists(path + \"_\" + count + \".json\")) { count++; } path += \"_\" + count + \".json\"; } else { path += \".json\"; } File.WriteAllText(path, JSON); } public void AddWindows(List rectangles) { foreach (Zone driverZone in MainZone.Zones) { Bitmap zoneImage = driverZone.ZoneImage; for (int i = 1; i <= rectangles.Count; i++) { switch (i) { case 1: //First zone should be the driver's Position driverZone.AddWindow(new DriverPositionWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 2: //First zone should be the Gap to leader driverZone.AddWindow(new DriverGapToLeaderWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 3: //First zone should be the driver's Lap Time driverZone.AddWindow(new DriverLapTimeWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 4: //First zone should be the driver's DRS status driverZone.AddWindow(new DriverDrsWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 5: //First zone should be the driver's Tyre's informations driverZone.AddWindow(new DriverTyresWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 6: //First zone should be the driver's Name driverZone.AddWindow(new DriverNameWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 7: //First zone should be the driver's First Sector driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 1, false)); break; case 8: //First zone should be the driver's Second Sector driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 2, false)); break; case 9: //First zone should be the driver's Position Sector driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 3, false)); break; } } } } public void AutoCalibrate() { List detectedText = new List(); List zones = new List(); TesseractEngine engine = new TesseractEngine(Window.TESS_DATA_FOLDER.FullName, \"eng\", EngineMode.Default); Image image = MainZone.ZoneImage; var tessImage = Pix.LoadFromMemory(Window.ImageToByte(image)); Page page = engine.Process(tessImage); using (var iter = page.GetIterator()) { iter.Begin(); do { Rect boundingBox; if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox)) { //var text = iter.GetText(PageIteratorLevel.Word).ToUpper(); //We remove all the rectangles that are definitely too big if (boundingBox.Height < image.Height / NUMBER_OF_DRIVERS) { //Now we add a filter to only get the boxes in the right because they are much more reliable in size if (boundingBox.X1 > image.Width / 2) { //Now we check if an other square box has been found roughly in the same y axis bool match = false; //The tolerance is roughly half the size that a window will be int tolerance = (image.Height / NUMBER_OF_DRIVERS) / 2; foreach (Rectangle rect in detectedText) { if (rect.Y > boundingBox.Y1 - tolerance && rect.Y < boundingBox.Y1 + tolerance) { //There already is a rectangle in this line match = true; } } //if nothing matched we can add it if (!match) detectedText.Add(new Rectangle(boundingBox.X1, boundingBox.Y1, boundingBox.Width, boundingBox.Height)); } } } } while (iter.Next(PageIteratorLevel.Word)); } //DEBUG int i = 1; foreach (Rectangle Rectangle in detectedText) { Rectangle windowRectangle; Size windowSize = new Size(image.Width, image.Height / NUMBER_OF_DRIVERS); Point windowLocation = new Point(0, (Rectangle.Y + Rectangle.Height / 2) - windowSize.Height / 2); windowRectangle = new Rectangle(windowLocation, windowSize); //We add the driver zones Zone driverZone = new Zone(MainZone.ZoneImage, windowRectangle, \"DriverZone\"); MainZone.AddZone(driverZone); driverZone.ZoneImage.Save(\"Driver\" + i+\".png\"); i++; } } } }","title":"ConfigurationTool.cs"},{"location":"Code/ConfigurationTool.html#configurationtoolcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : ConfigurationTool.cs /// Brief : Class that contains all the methods needed to create a config file for the OCR /// Version : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using Tesseract; using System.IO; namespace Test_Merge { public class ConfigurationTool { public Zone MainZone; public const int NUMBER_OF_DRIVERS = 20; public const int NUMBER_OF_ZONES = 9; public const string CONFIGS_FOLDER_NAME = \"./Presets/\"; public ConfigurationTool(Bitmap fullImage, Rectangle mainZoneDimensions) { MainZone = new Zone(fullImage, mainZoneDimensions,\"Main\"); AutoCalibrate(); } public void ResetMainZone() { MainZone.ResetZones(); } public void ResetWindows() { MainZone.ResetWindows(); } public void SaveToJson(List drivers, string configName) { string JSON = \"\"; JSON += \"{\" + Environment.NewLine; JSON += MainZone.ToJSON() + \",\" + Environment.NewLine; JSON += \"\\\"Drivers\\\":[\" + Environment.NewLine; for (int i = 0; i < drivers.Count; i++) { JSON += \"\\\"\" + drivers[i] + \"\\\"\"; if (i < drivers.Count - 1) JSON += \",\"; JSON += Environment.NewLine; } JSON += \"]\" + Environment.NewLine; JSON += \"}\"; if (!Directory.Exists(CONFIGS_FOLDER_NAME)) Directory.CreateDirectory(CONFIGS_FOLDER_NAME); string path = CONFIGS_FOLDER_NAME + configName; if (File.Exists(path + \".json\")) { //We need to create a new name int count = 2; while (File.Exists(path + \"_\" + count + \".json\")) { count++; } path += \"_\" + count + \".json\"; } else { path += \".json\"; } File.WriteAllText(path, JSON); } public void AddWindows(List rectangles) { foreach (Zone driverZone in MainZone.Zones) { Bitmap zoneImage = driverZone.ZoneImage; for (int i = 1; i <= rectangles.Count; i++) { switch (i) { case 1: //First zone should be the driver's Position driverZone.AddWindow(new DriverPositionWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 2: //First zone should be the Gap to leader driverZone.AddWindow(new DriverGapToLeaderWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 3: //First zone should be the driver's Lap Time driverZone.AddWindow(new DriverLapTimeWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 4: //First zone should be the driver's DRS status driverZone.AddWindow(new DriverDrsWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 5: //First zone should be the driver's Tyre's informations driverZone.AddWindow(new DriverTyresWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 6: //First zone should be the driver's Name driverZone.AddWindow(new DriverNameWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 7: //First zone should be the driver's First Sector driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 1, false)); break; case 8: //First zone should be the driver's Second Sector driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 2, false)); break; case 9: //First zone should be the driver's Position Sector driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 3, false)); break; } } } } public void AutoCalibrate() { List detectedText = new List(); List zones = new List(); TesseractEngine engine = new TesseractEngine(Window.TESS_DATA_FOLDER.FullName, \"eng\", EngineMode.Default); Image image = MainZone.ZoneImage; var tessImage = Pix.LoadFromMemory(Window.ImageToByte(image)); Page page = engine.Process(tessImage); using (var iter = page.GetIterator()) { iter.Begin(); do { Rect boundingBox; if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox)) { //var text = iter.GetText(PageIteratorLevel.Word).ToUpper(); //We remove all the rectangles that are definitely too big if (boundingBox.Height < image.Height / NUMBER_OF_DRIVERS) { //Now we add a filter to only get the boxes in the right because they are much more reliable in size if (boundingBox.X1 > image.Width / 2) { //Now we check if an other square box has been found roughly in the same y axis bool match = false; //The tolerance is roughly half the size that a window will be int tolerance = (image.Height / NUMBER_OF_DRIVERS) / 2; foreach (Rectangle rect in detectedText) { if (rect.Y > boundingBox.Y1 - tolerance && rect.Y < boundingBox.Y1 + tolerance) { //There already is a rectangle in this line match = true; } } //if nothing matched we can add it if (!match) detectedText.Add(new Rectangle(boundingBox.X1, boundingBox.Y1, boundingBox.Width, boundingBox.Height)); } } } } while (iter.Next(PageIteratorLevel.Word)); } //DEBUG int i = 1; foreach (Rectangle Rectangle in detectedText) { Rectangle windowRectangle; Size windowSize = new Size(image.Width, image.Height / NUMBER_OF_DRIVERS); Point windowLocation = new Point(0, (Rectangle.Y + Rectangle.Height / 2) - windowSize.Height / 2); windowRectangle = new Rectangle(windowLocation, windowSize); //We add the driver zones Zone driverZone = new Zone(MainZone.ZoneImage, windowRectangle, \"DriverZone\"); MainZone.AddZone(driverZone); driverZone.ZoneImage.Save(\"Driver\" + i+\".png\"); i++; } } } }","title":"ConfigurationTool.cs"},{"location":"Code/DriverGapToLeaderWindow.html","text":"DriverGapToLeaderWindow.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverGapToLeaderWindow.cs /// Brief : Window containing infos about the gap to the leader of a driver /// Version : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Test_Merge { internal class DriverGapToLeaderWindow:Window { public DriverGapToLeaderWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"GapToLeader\"; } /// /// Decodes the gap to leader using Tesseract OCR /// /// public override async Task DecodePng() { int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Gap, Engine); return result; } } }","title":"DriverGapToLeaderWindow.cs"},{"location":"Code/DriverGapToLeaderWindow.html#drivergaptoleaderwindowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverGapToLeaderWindow.cs /// Brief : Window containing infos about the gap to the leader of a driver /// Version : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Test_Merge { internal class DriverGapToLeaderWindow:Window { public DriverGapToLeaderWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"GapToLeader\"; } /// /// Decodes the gap to leader using Tesseract OCR /// /// public override async Task DecodePng() { int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Gap, Engine); return result; } } }","title":"DriverGapToLeaderWindow.cs"},{"location":"Code/DriverPositionWindow.html","text":"DriverPositionWindow.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverPosition.cs /// Brief : Window containing infos about the position of a driver. /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { public class DriverPositionWindow:Window { public DriverPositionWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Position\"; } /// /// Decodes the position number using Tesseract OCR /// /// The position of the pilot in int public override async Task DecodePng() { string ocrResult = await GetStringFromPng(WindowImage, Engine, \"0123456789\"); int position; try { position = Convert.ToInt32(ocrResult); } catch { position = -1; } return position; } } }","title":"DriverPositionWindow.cs"},{"location":"Code/DriverPositionWindow.html#driverpositionwindowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverPosition.cs /// Brief : Window containing infos about the position of a driver. /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { public class DriverPositionWindow:Window { public DriverPositionWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Position\"; } /// /// Decodes the position number using Tesseract OCR /// /// The position of the pilot in int public override async Task DecodePng() { string ocrResult = await GetStringFromPng(WindowImage, Engine, \"0123456789\"); int position; try { position = Convert.ToInt32(ocrResult); } catch { position = -1; } return position; } } }","title":"DriverPositionWindow.cs"},{"location":"Code/F1TVEmulator.html","text":"F1TVEmulator.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : F1TVEmulator.cs /// Brief : Class that contains methods to emulate a browser and navigate the F1TV website /// Version : 0.1 using OpenQA.Selenium; using OpenQA.Selenium.Firefox; using OpenQA.Selenium.Interactions; using OpenQA.Selenium.Support.UI; using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Test_Merge { internal class F1TVEmulator { public const string COOKIE_HOST = \".formula1.com\"; public const string PYTHON_COOKIE_RETRIEVAL_FILENAME = \"recoverCookiesCSV.py\"; public const string GECKODRIVER_FILENAME = @\"geckodriver-v0.27.0-win64\\geckodriver.exe\"; //BE CAREFULL IF YOU CHANGE IT HERE YOU NEED TO CHANGE IT IN THE PYTHON SCRIPT TOO public const string COOKIES_CSV_FILENAME = \"cookies.csv\"; private FirefoxDriver Driver; private bool _ready; private string _grandPrixUrl; public string GrandPrixUrl { get => _grandPrixUrl; private set => _grandPrixUrl = value; } public bool Ready { get => _ready; set => _ready = value; } public F1TVEmulator(string grandPrixUrl) { GrandPrixUrl = grandPrixUrl; Ready = false; } private void StartCookieRecovering() { string scriptPath = PYTHON_COOKIE_RETRIEVAL_FILENAME; Process process = new Process(); process.StartInfo.FileName = \"python.exe\"; process.StartInfo.Arguments = scriptPath; process.StartInfo.UseShellExecute = false; process.StartInfo.RedirectStandardOutput = true; process.Start(); string output = process.StandardOutput.ReadToEnd(); process.WaitForExit(); } public string GetCookie(string host, string name) { StartCookieRecovering(); string value = \"\"; List cookies = new List(); using (var reader = new StreamReader(COOKIES_CSV_FILENAME)) { // Read the header row and validate column order string header = reader.ReadLine(); string[] expectedColumns = { \"host_key\", \"name\", \"value\", \"path\", \"expires_utc\", \"is_secure\", \"is_httponly\" }; string[] actualColumns = header.Split(','); for (int i = 0; i < expectedColumns.Length; i++) { if (expectedColumns[i] != actualColumns[i]) { throw new InvalidOperationException($\"Expected column '{expectedColumns[i]}' at index {i} but found '{actualColumns[i]}'\"); } } // Read each data row and parse values into a Cookie object while (!reader.EndOfStream) { string line = reader.ReadLine(); string[] fields = line.Split(','); string hostname = fields[0]; string cookieName = fields[1]; if (hostname == host && cookieName == name) { value = fields[2]; } } } return value; } public async Task Start() { Ready = false; string loginCookieName = \"login\"; string loginSessionCookieName = \"login-session\"; string loginCookieValue = GetCookie(COOKIE_HOST, loginCookieName); string loginSessionValue = GetCookie(COOKIE_HOST, loginSessionCookieName); int windowWidth = 1920; int windowHeight = 768; var service = FirefoxDriverService.CreateDefaultService(GECKODRIVER_FILENAME); service.Host = \"127.0.0.1\"; service.Port = 5555; FirefoxProfile profile = new FirefoxProfile(); FirefoxOptions options = new FirefoxOptions(); //profile.SetPreference(\"full-screen-api.ignore-widgets\", true); //profile.SetPreference(\"media.hardware-video-decoding.enabled\", true); //profile.SetPreference(\"full-screen-api.enabled\", true); options.Profile = profile; profile.SetPreference(\"layout.css.devPixelsPerPx\", \"1.0\"); options.AcceptInsecureCertificates = true; options.AddArgument(\"--headless\"); //options.AddArgument(\"--start-maximized\"); //options.AddArgument(\"--window-size=1920x1080\"); //options.AddArgument(\"--width=\" + windowWidth); //options.AddArgument(\"--height=\" + windowHeight); //options.AddArgument(\"-window-size=1920x1080\"); //options.AddArgument(\"--width=1920\"); //options.AddArgument(\"--height=1080\"); //profile try { Driver = new FirefoxDriver(service, options); } catch { Ready = false; return 101; } Actions actions = new Actions(Driver); var loginCookie = new Cookie(loginCookieName, loginCookieValue, COOKIE_HOST, \"/\", DateTime.Now.AddDays(5)); var loginSessionCookie = new Cookie(loginSessionCookieName, loginSessionValue, COOKIE_HOST, \"/\", DateTime.Now.AddDays(5)); Driver.Navigate().GoToUrl(\"https://f1tv.formula1.com/\"); Driver.Manage().Cookies.AddCookie(loginCookie); Driver.Manage().Cookies.AddCookie(loginSessionCookie); try { Driver.Navigate().GoToUrl(GrandPrixUrl); } catch { //The url is not a valid url Driver.Dispose(); return 103; } //Waits for the page to fully load Driver.Manage().Timeouts().PageLoad = TimeSpan.FromSeconds(30); //Removes the cookie prompt try { IWebElement conscentButton = Driver.FindElement(By.Id(\"truste-consent-button\")); conscentButton.Click(); } catch { //Could not locate the cookie button Screenshot(\"ERROR104\"); Driver.Dispose(); return 104; } //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); //Switches to the Data channel try { IWebElement dataChannelButton = Driver.FindElement(By.ClassName(\"data-button\")); dataChannelButton.Click(); } catch { //If the data button does not exists its because the user is not connected Screenshot(\"ERROR102\"); Driver.Dispose(); return 102; } //Open settings // Press the space key, this should make the setting button visible // It does not matter if the feed is paused because when changing channel it autoplays actions.SendKeys(OpenQA.Selenium.Keys.Space).Perform(); //Clicks on the settings Icon int tries = 0; bool success = false; while (tries < 100 && !success) { Thread.Sleep(100); try { IWebElement settingsButton = Driver.FindElement(By.ClassName(\"bmpui-ui-settingstogglebutton\")); settingsButton.Click(); IWebElement selectElement = Driver.FindElement(By.ClassName(\"bmpui-ui-videoqualityselectbox\")); SelectElement select = new SelectElement(selectElement); IWebElement selectOption = selectElement.FindElement(By.CssSelector(\"option[value^='1080_']\")); selectOption.Click(); success = true; } catch { //Sometimes it can crash because it could not get the options to show up in time. When it happens just retry success = false; tries++; } } if (!success) { Screenshot(\"ERROR105\"); Driver.Dispose(); return 105; } Screenshot(\"BEFOREFULLSCREEN\"); //Makes the feed fullscreen //Driver.Manage().Window.Size = new System.Drawing.Size(windowWidth, windowHeight); Driver.Manage().Window.Maximize(); WebDriverWait wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); try { IWebElement fullScreenButton = Driver.FindElement(By.ClassName(\"bmpui-ui-fullscreentogglebutton\")); fullScreenButton.Click(); } catch { Screenshot(\"ERROR106\"); Driver.Dispose(); return 106; } Screenshot(\"AFTERFULLSCREEN\"); //STARTUP FINISHED READY TO SCREENSHOT Ready = true; return 0; } public Bitmap Screenshot(string name = \"TEST\") { Bitmap result = new Bitmap(4242, 6969); try { //Screenshot scrsht = ((ITakesScreenshot)Driver).GetScreenshot(); //profileriver.SetPreference(\"layout.css.devPixelsPerPx\", \"1.0\"); //Screenshot scrsht = Driver.GetFullPageScreenshot(); Screenshot scrsht = Driver.GetScreenshot(); byte[] screenshotBytes = Convert.FromBase64String(scrsht.AsBase64EncodedString); MemoryStream stream = new MemoryStream(screenshotBytes); result = new Bitmap(stream); //result.Save(name + \".png\"); scrsht.SaveAsFile(name + \".png\"); } catch { //Nothing for now } return result; } public void Stop() { Ready = false; Driver.Dispose(); } public void ResetDriver() { Ready = false; Driver.Dispose(); Driver = null; } } }","title":"F1TVEmulator.cs"},{"location":"Code/F1TVEmulator.html#f1tvemulatorcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : F1TVEmulator.cs /// Brief : Class that contains methods to emulate a browser and navigate the F1TV website /// Version : 0.1 using OpenQA.Selenium; using OpenQA.Selenium.Firefox; using OpenQA.Selenium.Interactions; using OpenQA.Selenium.Support.UI; using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Test_Merge { internal class F1TVEmulator { public const string COOKIE_HOST = \".formula1.com\"; public const string PYTHON_COOKIE_RETRIEVAL_FILENAME = \"recoverCookiesCSV.py\"; public const string GECKODRIVER_FILENAME = @\"geckodriver-v0.27.0-win64\\geckodriver.exe\"; //BE CAREFULL IF YOU CHANGE IT HERE YOU NEED TO CHANGE IT IN THE PYTHON SCRIPT TOO public const string COOKIES_CSV_FILENAME = \"cookies.csv\"; private FirefoxDriver Driver; private bool _ready; private string _grandPrixUrl; public string GrandPrixUrl { get => _grandPrixUrl; private set => _grandPrixUrl = value; } public bool Ready { get => _ready; set => _ready = value; } public F1TVEmulator(string grandPrixUrl) { GrandPrixUrl = grandPrixUrl; Ready = false; } private void StartCookieRecovering() { string scriptPath = PYTHON_COOKIE_RETRIEVAL_FILENAME; Process process = new Process(); process.StartInfo.FileName = \"python.exe\"; process.StartInfo.Arguments = scriptPath; process.StartInfo.UseShellExecute = false; process.StartInfo.RedirectStandardOutput = true; process.Start(); string output = process.StandardOutput.ReadToEnd(); process.WaitForExit(); } public string GetCookie(string host, string name) { StartCookieRecovering(); string value = \"\"; List cookies = new List(); using (var reader = new StreamReader(COOKIES_CSV_FILENAME)) { // Read the header row and validate column order string header = reader.ReadLine(); string[] expectedColumns = { \"host_key\", \"name\", \"value\", \"path\", \"expires_utc\", \"is_secure\", \"is_httponly\" }; string[] actualColumns = header.Split(','); for (int i = 0; i < expectedColumns.Length; i++) { if (expectedColumns[i] != actualColumns[i]) { throw new InvalidOperationException($\"Expected column '{expectedColumns[i]}' at index {i} but found '{actualColumns[i]}'\"); } } // Read each data row and parse values into a Cookie object while (!reader.EndOfStream) { string line = reader.ReadLine(); string[] fields = line.Split(','); string hostname = fields[0]; string cookieName = fields[1]; if (hostname == host && cookieName == name) { value = fields[2]; } } } return value; } public async Task Start() { Ready = false; string loginCookieName = \"login\"; string loginSessionCookieName = \"login-session\"; string loginCookieValue = GetCookie(COOKIE_HOST, loginCookieName); string loginSessionValue = GetCookie(COOKIE_HOST, loginSessionCookieName); int windowWidth = 1920; int windowHeight = 768; var service = FirefoxDriverService.CreateDefaultService(GECKODRIVER_FILENAME); service.Host = \"127.0.0.1\"; service.Port = 5555; FirefoxProfile profile = new FirefoxProfile(); FirefoxOptions options = new FirefoxOptions(); //profile.SetPreference(\"full-screen-api.ignore-widgets\", true); //profile.SetPreference(\"media.hardware-video-decoding.enabled\", true); //profile.SetPreference(\"full-screen-api.enabled\", true); options.Profile = profile; profile.SetPreference(\"layout.css.devPixelsPerPx\", \"1.0\"); options.AcceptInsecureCertificates = true; options.AddArgument(\"--headless\"); //options.AddArgument(\"--start-maximized\"); //options.AddArgument(\"--window-size=1920x1080\"); //options.AddArgument(\"--width=\" + windowWidth); //options.AddArgument(\"--height=\" + windowHeight); //options.AddArgument(\"-window-size=1920x1080\"); //options.AddArgument(\"--width=1920\"); //options.AddArgument(\"--height=1080\"); //profile try { Driver = new FirefoxDriver(service, options); } catch { Ready = false; return 101; } Actions actions = new Actions(Driver); var loginCookie = new Cookie(loginCookieName, loginCookieValue, COOKIE_HOST, \"/\", DateTime.Now.AddDays(5)); var loginSessionCookie = new Cookie(loginSessionCookieName, loginSessionValue, COOKIE_HOST, \"/\", DateTime.Now.AddDays(5)); Driver.Navigate().GoToUrl(\"https://f1tv.formula1.com/\"); Driver.Manage().Cookies.AddCookie(loginCookie); Driver.Manage().Cookies.AddCookie(loginSessionCookie); try { Driver.Navigate().GoToUrl(GrandPrixUrl); } catch { //The url is not a valid url Driver.Dispose(); return 103; } //Waits for the page to fully load Driver.Manage().Timeouts().PageLoad = TimeSpan.FromSeconds(30); //Removes the cookie prompt try { IWebElement conscentButton = Driver.FindElement(By.Id(\"truste-consent-button\")); conscentButton.Click(); } catch { //Could not locate the cookie button Screenshot(\"ERROR104\"); Driver.Dispose(); return 104; } //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); //Switches to the Data channel try { IWebElement dataChannelButton = Driver.FindElement(By.ClassName(\"data-button\")); dataChannelButton.Click(); } catch { //If the data button does not exists its because the user is not connected Screenshot(\"ERROR102\"); Driver.Dispose(); return 102; } //Open settings // Press the space key, this should make the setting button visible // It does not matter if the feed is paused because when changing channel it autoplays actions.SendKeys(OpenQA.Selenium.Keys.Space).Perform(); //Clicks on the settings Icon int tries = 0; bool success = false; while (tries < 100 && !success) { Thread.Sleep(100); try { IWebElement settingsButton = Driver.FindElement(By.ClassName(\"bmpui-ui-settingstogglebutton\")); settingsButton.Click(); IWebElement selectElement = Driver.FindElement(By.ClassName(\"bmpui-ui-videoqualityselectbox\")); SelectElement select = new SelectElement(selectElement); IWebElement selectOption = selectElement.FindElement(By.CssSelector(\"option[value^='1080_']\")); selectOption.Click(); success = true; } catch { //Sometimes it can crash because it could not get the options to show up in time. When it happens just retry success = false; tries++; } } if (!success) { Screenshot(\"ERROR105\"); Driver.Dispose(); return 105; } Screenshot(\"BEFOREFULLSCREEN\"); //Makes the feed fullscreen //Driver.Manage().Window.Size = new System.Drawing.Size(windowWidth, windowHeight); Driver.Manage().Window.Maximize(); WebDriverWait wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); try { IWebElement fullScreenButton = Driver.FindElement(By.ClassName(\"bmpui-ui-fullscreentogglebutton\")); fullScreenButton.Click(); } catch { Screenshot(\"ERROR106\"); Driver.Dispose(); return 106; } Screenshot(\"AFTERFULLSCREEN\"); //STARTUP FINISHED READY TO SCREENSHOT Ready = true; return 0; } public Bitmap Screenshot(string name = \"TEST\") { Bitmap result = new Bitmap(4242, 6969); try { //Screenshot scrsht = ((ITakesScreenshot)Driver).GetScreenshot(); //profileriver.SetPreference(\"layout.css.devPixelsPerPx\", \"1.0\"); //Screenshot scrsht = Driver.GetFullPageScreenshot(); Screenshot scrsht = Driver.GetScreenshot(); byte[] screenshotBytes = Convert.FromBase64String(scrsht.AsBase64EncodedString); MemoryStream stream = new MemoryStream(screenshotBytes); result = new Bitmap(stream); //result.Save(name + \".png\"); scrsht.SaveAsFile(name + \".png\"); } catch { //Nothing for now } return result; } public void Stop() { Ready = false; Driver.Dispose(); } public void ResetDriver() { Ready = false; Driver.Dispose(); Driver = null; } } }","title":"F1TVEmulator.cs"},{"location":"Code/Program.html","text":"Program.cs \ufeffusing System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Windows.Forms; namespace Test_Merge { internal static class Program { /// /// The main entry point for the application. /// [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } } }","title":"Program.cs"},{"location":"Code/Program.html#programcs","text":"\ufeffusing System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Windows.Forms; namespace Test_Merge { internal static class Program { /// /// The main entry point for the application. /// [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } } }","title":"Program.cs"},{"location":"Code/Window.html","text":"Window.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : Window.cs /// Brief : Default Window object that is mainly expected to be inherited. /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; using System.IO; using Tesseract; using System.Text.RegularExpressions; using System.Drawing.Drawing2D; namespace Test_Merge { public class Window { private Rectangle _bounds; private Bitmap _image; private string _name; protected TesseractEngine Engine; public Rectangle Bounds { get => _bounds; private set => _bounds = value; } public Bitmap Image { get => _image; set => _image = value; } public string Name { get => _name; protected set => _name = value; } //This will have to be changed if you want to make it run on your machine public static DirectoryInfo TESS_DATA_FOLDER = new DirectoryInfo(@\"C:\\Users\\Moi\\Pictures\\SeleniumScreens\\TessData\"); public Bitmap WindowImage { get { //This little trickery lets you have the image that the window sees Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); return sample; } } public Window(Bitmap image, Rectangle bounds, bool generateEngine = true) { Image = image; Bounds = bounds; if (generateEngine) { Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, \"eng\", EngineMode.Default); Engine.DefaultPageSegMode = PageSegMode.SingleLine; } } /// /// Method that will have to be used by the childrens to let the model make them decode the images they have /// /// Returns an object because we dont know what kind of return it will be public virtual async Task DecodePng() { return \"NaN\"; } /// /// Method that will have to be used by the childrens to let the model make them decode the images they have /// /// This is a list of the different possible drivers in the race. It should not be too big but NEVER be too short /// Returns an object because we dont know what kind of return it will be public virtual async Task DecodePng(List driverList) { return \"NaN\"; } /// /// This converts an image into a byte[]. It can be usefull when doing unsafe stuff. Use at your own risks /// /// The image you want to convert /// A byte array containing the image informations public static byte[] ImageToByte(Image inputImage) { using (var stream = new MemoryStream()) { inputImage.Save(stream, System.Drawing.Imaging.ImageFormat.Png); return stream.ToArray(); } } /// /// This method is used to recover a time from a PNG using Tesseract OCR /// /// The image where the text is /// The type of window it is /// The Tesseract Engine /// The time in milliseconds public static async Task GetTimeFromPng(Bitmap windowImage, OcrImage.WindowType windowType, TesseractEngine Engine) { //Kind of a big method but it has a lot of error handling and has to work with three special cases string rawResult = \"\"; int result = 0; switch (windowType) { case OcrImage.WindowType.Sector: //The usual sector is in this form : 33.456 Engine.SetVariable(\"tessedit_char_whitelist\", \"0123456789.\"); break; case OcrImage.WindowType.LapTime: //The usual Lap time is in this form : 1:45:345 Engine.SetVariable(\"tessedit_char_whitelist\", \"0123456789.:\"); break; case OcrImage.WindowType.Gap: //The usual Gap is in this form : + 34.567 Engine.SetVariable(\"tessedit_char_whitelist\", \"0123456789.+\"); break; default: Engine.SetVariable(\"tessedit_char_whitelist\", \"\"); break; } Bitmap enhancedImage = new OcrImage(windowImage).Enhance(windowType); var tessImage = Pix.LoadFromMemory(ImageToByte(enhancedImage)); Page page = Engine.Process(tessImage); Graphics g = Graphics.FromImage(enhancedImage); // Get the iterator for the page layout using (var iter = page.GetIterator()) { // Loop over the elements of the page layout iter.Begin(); do { // Get the text for the current element try { rawResult += iter.GetText(PageIteratorLevel.Word); } catch { //nothing we just dont add it if its not a number } } while (iter.Next(PageIteratorLevel.Word)); } List rawNumbers; //In the gaps we can find '+' but we dont care about it its redondant a driver will never be - something if (windowType == OcrImage.WindowType.Gap) rawResult = Regex.Replace(rawResult, \"[^0-9.:]\", \"\"); //Splits into minuts seconds miliseconds rawNumbers = rawResult.Split('.', ':').ToList(); //removes any empty cells (tho this usually sign of a really bad OCR implementation tbh will have to be fixed higher in the chian) rawNumbers.RemoveAll(x => ((string)x) == \"\"); if (rawNumbers.Count == 3) { //mm:ss:ms result = (Convert.ToInt32(rawNumbers[0]) * 1000 * 60) + (Convert.ToInt32(rawNumbers[1]) * 1000) + Convert.ToInt32(rawNumbers[2]); } else { if (rawNumbers.Count == 2) { //ss:ms result = (Convert.ToInt32(rawNumbers[0]) * 1000) + Convert.ToInt32(rawNumbers[1]); if (result > 999999) { //We know that we have way too much seconds to make a minut //Its usually because the \":\" have been interpreted as a number int minuts = (int)(rawNumbers[0][0] - '0'); // rawNumbers[0][1] should contain the : that has been mistaken int seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString()); int ms = Convert.ToInt32(rawNumbers[1]); result = (Convert.ToInt32(minuts) * 1000 * 60) + (Convert.ToInt32(seconds) * 1000) + Convert.ToInt32(ms); } } else { if (rawNumbers.Count == 1) { try { result = Convert.ToInt32(rawNumbers[0]); } catch { //It can be because the input is empty or because its the LEADER bracket result = 0; } } else { //Auuuugh result = 0; } } } page.Dispose(); return result; } /// /// Method that recovers strings from an image using Tesseract OCR /// /// The image of the window that contains text /// The Tesseract engine /// The list of allowed chars /// The type of window the text is on. Depending on the context the OCR will behave differently /// the string it found public static async Task GetStringFromPng(Bitmap WindowImage, TesseractEngine Engine, string allowedChars = \"\", OcrImage.WindowType windowType = OcrImage.WindowType.Text) { string result = \"\"; Engine.SetVariable(\"tessedit_char_whitelist\", allowedChars); Bitmap rawData = WindowImage; Bitmap enhancedImage = new OcrImage(rawData).Enhance(windowType); Page page = Engine.Process(enhancedImage); using (var iter = page.GetIterator()) { iter.Begin(); do { result += iter.GetText(PageIteratorLevel.Word); } while (iter.Next(PageIteratorLevel.Word)); } page.Dispose(); return result; } /// /// Get a smaller image from a bigger one /// /// The big bitmap you want to get a part of /// The dimensions of the new bitmap /// The little bitmap protected Bitmap GetSmallBitmapFromBigOne(Bitmap inputBitmap, Rectangle newBitmapDimensions) { Bitmap sample = new Bitmap(newBitmapDimensions.Width, newBitmapDimensions.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(inputBitmap, new Rectangle(0, 0, sample.Width, sample.Height), newBitmapDimensions, GraphicsUnit.Pixel); return sample; } /// /// Returns the closest string from a list of options /// /// an array of all the possibilities /// the string you want to compare /// The closest option protected static string FindClosestMatch(List options, string testString) { var closestMatch = \"\"; var closestDistance = int.MaxValue; foreach (var item in options) { var distance = LevenshteinDistance(item, testString); if (distance < closestDistance) { closestMatch = item; closestDistance = distance; } } return closestMatch; } //This method has been generated with the help of ChatGPT /// /// Method that computes a score of distance between two strings /// /// The first string (order irrelevant) /// The second string (order irrelevant) /// The levenshtein distance protected static int LevenshteinDistance(string string1, string string2) { if (string.IsNullOrEmpty(string1)) { return string.IsNullOrEmpty(string2) ? 0 : string2.Length; } if (string.IsNullOrEmpty(string2)) { return string.IsNullOrEmpty(string1) ? 0 : string1.Length; } var d = new int[string1.Length + 1, string2.Length + 1]; for (var i = 0; i <= string1.Length; i++) { d[i, 0] = i; } for (var j = 0; j <= string2.Length; j++) { d[0, j] = j; } for (var i = 1; i <= string1.Length; i++) { for (var j = 1; j <= string2.Length; j++) { var cost = (string1[i - 1] == string2[j - 1]) ? 0 : 1; d[i, j] = Math.Min(Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost); } } return d[string1.Length, string2.Length]; } public virtual string ToJSON() { string result = \"\"; result += \"\\\"\" + Name + \"\\\"\" + \":{\" + Environment.NewLine; result += \"\\t\" + \"\\\"x\\\":\" + Bounds.X + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"y\\\":\" + Bounds.Y + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"width\\\":\" + Bounds.Width + Environment.NewLine; result += \"}\"; return result; } } }","title":"Window.cs"},{"location":"Code/Window.html#windowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : Window.cs /// Brief : Default Window object that is mainly expected to be inherited. /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; using System.IO; using Tesseract; using System.Text.RegularExpressions; using System.Drawing.Drawing2D; namespace Test_Merge { public class Window { private Rectangle _bounds; private Bitmap _image; private string _name; protected TesseractEngine Engine; public Rectangle Bounds { get => _bounds; private set => _bounds = value; } public Bitmap Image { get => _image; set => _image = value; } public string Name { get => _name; protected set => _name = value; } //This will have to be changed if you want to make it run on your machine public static DirectoryInfo TESS_DATA_FOLDER = new DirectoryInfo(@\"C:\\Users\\Moi\\Pictures\\SeleniumScreens\\TessData\"); public Bitmap WindowImage { get { //This little trickery lets you have the image that the window sees Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); return sample; } } public Window(Bitmap image, Rectangle bounds, bool generateEngine = true) { Image = image; Bounds = bounds; if (generateEngine) { Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, \"eng\", EngineMode.Default); Engine.DefaultPageSegMode = PageSegMode.SingleLine; } } /// /// Method that will have to be used by the childrens to let the model make them decode the images they have /// /// Returns an object because we dont know what kind of return it will be public virtual async Task DecodePng() { return \"NaN\"; } /// /// Method that will have to be used by the childrens to let the model make them decode the images they have /// /// This is a list of the different possible drivers in the race. It should not be too big but NEVER be too short /// Returns an object because we dont know what kind of return it will be public virtual async Task DecodePng(List driverList) { return \"NaN\"; } /// /// This converts an image into a byte[]. It can be usefull when doing unsafe stuff. Use at your own risks /// /// The image you want to convert /// A byte array containing the image informations public static byte[] ImageToByte(Image inputImage) { using (var stream = new MemoryStream()) { inputImage.Save(stream, System.Drawing.Imaging.ImageFormat.Png); return stream.ToArray(); } } /// /// This method is used to recover a time from a PNG using Tesseract OCR /// /// The image where the text is /// The type of window it is /// The Tesseract Engine /// The time in milliseconds public static async Task GetTimeFromPng(Bitmap windowImage, OcrImage.WindowType windowType, TesseractEngine Engine) { //Kind of a big method but it has a lot of error handling and has to work with three special cases string rawResult = \"\"; int result = 0; switch (windowType) { case OcrImage.WindowType.Sector: //The usual sector is in this form : 33.456 Engine.SetVariable(\"tessedit_char_whitelist\", \"0123456789.\"); break; case OcrImage.WindowType.LapTime: //The usual Lap time is in this form : 1:45:345 Engine.SetVariable(\"tessedit_char_whitelist\", \"0123456789.:\"); break; case OcrImage.WindowType.Gap: //The usual Gap is in this form : + 34.567 Engine.SetVariable(\"tessedit_char_whitelist\", \"0123456789.+\"); break; default: Engine.SetVariable(\"tessedit_char_whitelist\", \"\"); break; } Bitmap enhancedImage = new OcrImage(windowImage).Enhance(windowType); var tessImage = Pix.LoadFromMemory(ImageToByte(enhancedImage)); Page page = Engine.Process(tessImage); Graphics g = Graphics.FromImage(enhancedImage); // Get the iterator for the page layout using (var iter = page.GetIterator()) { // Loop over the elements of the page layout iter.Begin(); do { // Get the text for the current element try { rawResult += iter.GetText(PageIteratorLevel.Word); } catch { //nothing we just dont add it if its not a number } } while (iter.Next(PageIteratorLevel.Word)); } List rawNumbers; //In the gaps we can find '+' but we dont care about it its redondant a driver will never be - something if (windowType == OcrImage.WindowType.Gap) rawResult = Regex.Replace(rawResult, \"[^0-9.:]\", \"\"); //Splits into minuts seconds miliseconds rawNumbers = rawResult.Split('.', ':').ToList(); //removes any empty cells (tho this usually sign of a really bad OCR implementation tbh will have to be fixed higher in the chian) rawNumbers.RemoveAll(x => ((string)x) == \"\"); if (rawNumbers.Count == 3) { //mm:ss:ms result = (Convert.ToInt32(rawNumbers[0]) * 1000 * 60) + (Convert.ToInt32(rawNumbers[1]) * 1000) + Convert.ToInt32(rawNumbers[2]); } else { if (rawNumbers.Count == 2) { //ss:ms result = (Convert.ToInt32(rawNumbers[0]) * 1000) + Convert.ToInt32(rawNumbers[1]); if (result > 999999) { //We know that we have way too much seconds to make a minut //Its usually because the \":\" have been interpreted as a number int minuts = (int)(rawNumbers[0][0] - '0'); // rawNumbers[0][1] should contain the : that has been mistaken int seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString()); int ms = Convert.ToInt32(rawNumbers[1]); result = (Convert.ToInt32(minuts) * 1000 * 60) + (Convert.ToInt32(seconds) * 1000) + Convert.ToInt32(ms); } } else { if (rawNumbers.Count == 1) { try { result = Convert.ToInt32(rawNumbers[0]); } catch { //It can be because the input is empty or because its the LEADER bracket result = 0; } } else { //Auuuugh result = 0; } } } page.Dispose(); return result; } /// /// Method that recovers strings from an image using Tesseract OCR /// /// The image of the window that contains text /// The Tesseract engine /// The list of allowed chars /// The type of window the text is on. Depending on the context the OCR will behave differently /// the string it found public static async Task GetStringFromPng(Bitmap WindowImage, TesseractEngine Engine, string allowedChars = \"\", OcrImage.WindowType windowType = OcrImage.WindowType.Text) { string result = \"\"; Engine.SetVariable(\"tessedit_char_whitelist\", allowedChars); Bitmap rawData = WindowImage; Bitmap enhancedImage = new OcrImage(rawData).Enhance(windowType); Page page = Engine.Process(enhancedImage); using (var iter = page.GetIterator()) { iter.Begin(); do { result += iter.GetText(PageIteratorLevel.Word); } while (iter.Next(PageIteratorLevel.Word)); } page.Dispose(); return result; } /// /// Get a smaller image from a bigger one /// /// The big bitmap you want to get a part of /// The dimensions of the new bitmap /// The little bitmap protected Bitmap GetSmallBitmapFromBigOne(Bitmap inputBitmap, Rectangle newBitmapDimensions) { Bitmap sample = new Bitmap(newBitmapDimensions.Width, newBitmapDimensions.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(inputBitmap, new Rectangle(0, 0, sample.Width, sample.Height), newBitmapDimensions, GraphicsUnit.Pixel); return sample; } /// /// Returns the closest string from a list of options /// /// an array of all the possibilities /// the string you want to compare /// The closest option protected static string FindClosestMatch(List options, string testString) { var closestMatch = \"\"; var closestDistance = int.MaxValue; foreach (var item in options) { var distance = LevenshteinDistance(item, testString); if (distance < closestDistance) { closestMatch = item; closestDistance = distance; } } return closestMatch; } //This method has been generated with the help of ChatGPT /// /// Method that computes a score of distance between two strings /// /// The first string (order irrelevant) /// The second string (order irrelevant) /// The levenshtein distance protected static int LevenshteinDistance(string string1, string string2) { if (string.IsNullOrEmpty(string1)) { return string.IsNullOrEmpty(string2) ? 0 : string2.Length; } if (string.IsNullOrEmpty(string2)) { return string.IsNullOrEmpty(string1) ? 0 : string1.Length; } var d = new int[string1.Length + 1, string2.Length + 1]; for (var i = 0; i <= string1.Length; i++) { d[i, 0] = i; } for (var j = 0; j <= string2.Length; j++) { d[0, j] = j; } for (var i = 1; i <= string1.Length; i++) { for (var j = 1; j <= string2.Length; j++) { var cost = (string1[i - 1] == string2[j - 1]) ? 0 : 1; d[i, j] = Math.Min(Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost); } } return d[string1.Length, string2.Length]; } public virtual string ToJSON() { string result = \"\"; result += \"\\\"\" + Name + \"\\\"\" + \":{\" + Environment.NewLine; result += \"\\t\" + \"\\\"x\\\":\" + Bounds.X + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"y\\\":\" + Bounds.Y + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"width\\\":\" + Bounds.Width + Environment.NewLine; result += \"}\"; return result; } } }","title":"Window.cs"},{"location":"Code/DriverData.html","text":"DriverData.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverData.cs /// Brief : Class used to store Driver informations /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Test_Merge { public class DriverData { public bool DRS; //True = Drs is opened public int GapToLeader; //In ms public int LapTime; //In ms public string Name; //Ex: LECLERC public int Position; //Ex: 1 public int Sector1; //in ms public int Sector2; //in ms public int Sector3; //in ms public Tyre CurrentTyre;//Ex Soft 11 laps public DriverData(bool dRS, int gapToLeader, int lapTime, string name, int position, int sector1, int sector2, int sector3, Tyre tyre) { DRS = dRS; GapToLeader = gapToLeader; LapTime = lapTime; Name = name; Position = position; Sector1 = sector1; Sector2 = sector2; Sector3 = sector3; CurrentTyre = tyre; } public DriverData() { DRS = false; GapToLeader = -1; LapTime = -1; Name = \"Unknown\"; Position = -1; Sector1 = -1; Sector2 = -1; Sector3 = -1; CurrentTyre = new Tyre(Tyre.Type.Undefined, -1); } /// /// Method that displays all the data found in a string /// /// string containing all the driver datas public override string ToString() { string result = \"\"; //Position result += \"Position : \" + Position + Environment.NewLine; //Gap result += \"GapToLeader : \" + Reader.ConvertMsToTime(GapToLeader) + Environment.NewLine; //LapTime result += \"LapTime : \" + Reader.ConvertMsToTime(LapTime) + Environment.NewLine; //DRS result += \"DRS : \" + DRS + Environment.NewLine; //Tyres result += \"Uses \" + CurrentTyre.Coumpound + \" tyre \" + CurrentTyre.NumberOfLaps + \" laps old\" + Environment.NewLine; //Name result += \"DriverName : \" + Name + Environment.NewLine; //Sector 1 result += \"Sector1 : \" + Reader.ConvertMsToTime(Sector1) + Environment.NewLine; //Sector 1 result += \"Sector2 : \" + Reader.ConvertMsToTime(Sector2) + Environment.NewLine; //Sector 1 result += \"Sector3 : \" + Reader.ConvertMsToTime(Sector3) + Environment.NewLine; return result; } } //Structure to store tyres infos public struct Tyre { //If new tyres were to be added you will have to need to change this enum public enum Type { Soft, Medium, Hard, Inter, Wet, Undefined } public Type Coumpound; public int NumberOfLaps; public Tyre(Type type, int laps) { Coumpound = type; NumberOfLaps = laps; } } }","title":"DriverData.cs"},{"location":"Code/DriverData.html#driverdatacs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverData.cs /// Brief : Class used to store Driver informations /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Test_Merge { public class DriverData { public bool DRS; //True = Drs is opened public int GapToLeader; //In ms public int LapTime; //In ms public string Name; //Ex: LECLERC public int Position; //Ex: 1 public int Sector1; //in ms public int Sector2; //in ms public int Sector3; //in ms public Tyre CurrentTyre;//Ex Soft 11 laps public DriverData(bool dRS, int gapToLeader, int lapTime, string name, int position, int sector1, int sector2, int sector3, Tyre tyre) { DRS = dRS; GapToLeader = gapToLeader; LapTime = lapTime; Name = name; Position = position; Sector1 = sector1; Sector2 = sector2; Sector3 = sector3; CurrentTyre = tyre; } public DriverData() { DRS = false; GapToLeader = -1; LapTime = -1; Name = \"Unknown\"; Position = -1; Sector1 = -1; Sector2 = -1; Sector3 = -1; CurrentTyre = new Tyre(Tyre.Type.Undefined, -1); } /// /// Method that displays all the data found in a string /// /// string containing all the driver datas public override string ToString() { string result = \"\"; //Position result += \"Position : \" + Position + Environment.NewLine; //Gap result += \"GapToLeader : \" + Reader.ConvertMsToTime(GapToLeader) + Environment.NewLine; //LapTime result += \"LapTime : \" + Reader.ConvertMsToTime(LapTime) + Environment.NewLine; //DRS result += \"DRS : \" + DRS + Environment.NewLine; //Tyres result += \"Uses \" + CurrentTyre.Coumpound + \" tyre \" + CurrentTyre.NumberOfLaps + \" laps old\" + Environment.NewLine; //Name result += \"DriverName : \" + Name + Environment.NewLine; //Sector 1 result += \"Sector1 : \" + Reader.ConvertMsToTime(Sector1) + Environment.NewLine; //Sector 1 result += \"Sector2 : \" + Reader.ConvertMsToTime(Sector2) + Environment.NewLine; //Sector 1 result += \"Sector3 : \" + Reader.ConvertMsToTime(Sector3) + Environment.NewLine; return result; } } //Structure to store tyres infos public struct Tyre { //If new tyres were to be added you will have to need to change this enum public enum Type { Soft, Medium, Hard, Inter, Wet, Undefined } public Type Coumpound; public int NumberOfLaps; public Tyre(Type type, int laps) { Coumpound = type; NumberOfLaps = laps; } } }","title":"DriverData.cs"},{"location":"Code/DriverLapTimeWindow.html","text":"DriverLapTimeWindow.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverLapTimeWindow /// Brief : Window containing infos about the lap time of a driver /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { internal class DriverLapTimeWindow:Window { public DriverLapTimeWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"LapTime\"; } /// /// Decodes the lap time contained in the image using OCR Tesseract /// /// The laptime in int (ms) public override async Task DecodePng() { int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.LapTime, Engine); return result; } } }","title":"DriverLapTimeWindow.cs"},{"location":"Code/DriverLapTimeWindow.html#driverlaptimewindowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverLapTimeWindow /// Brief : Window containing infos about the lap time of a driver /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { internal class DriverLapTimeWindow:Window { public DriverLapTimeWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"LapTime\"; } /// /// Decodes the lap time contained in the image using OCR Tesseract /// /// The laptime in int (ms) public override async Task DecodePng() { int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.LapTime, Engine); return result; } } }","title":"DriverLapTimeWindow.cs"},{"location":"Code/DriverSectorWindow.html","text":"DriverSectorWindow.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverSectorWindow.cs /// Brief : Window containing infos about a driver sector time. Can be the first second or third, does not matter. /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { internal class DriverSectorWindow:Window { public DriverSectorWindow(Bitmap image, Rectangle bounds, int sectorId, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Sector\"+sectorId; } /// /// Decodes the sector /// /// the sector time in int (ms) public override async Task DecodePng() { int ocrResult = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector, Engine); return ocrResult; } } }","title":"DriverSectorWindow.cs"},{"location":"Code/DriverSectorWindow.html#driversectorwindowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverSectorWindow.cs /// Brief : Window containing infos about a driver sector time. Can be the first second or third, does not matter. /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { internal class DriverSectorWindow:Window { public DriverSectorWindow(Bitmap image, Rectangle bounds, int sectorId, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Sector\"+sectorId; } /// /// Decodes the sector /// /// the sector time in int (ms) public override async Task DecodePng() { int ocrResult = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector, Engine); return ocrResult; } } }","title":"DriverSectorWindow.cs"},{"location":"Code/Form1.html","text":"Form1.cs \ufeffusing System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace Test_Merge { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void btnSettings_Click(object sender, EventArgs e) { Settings settingsForm = new Settings(); settingsForm.ShowDialog(); MessageBox.Show(settingsForm.GrandPrixUrl + Environment.NewLine + settingsForm.GrandPrixName + Environment.NewLine + settingsForm.GrandPrixYear); } } }","title":"Form1.cs"},{"location":"Code/Form1.html#form1cs","text":"\ufeffusing System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace Test_Merge { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void btnSettings_Click(object sender, EventArgs e) { Settings settingsForm = new Settings(); settingsForm.ShowDialog(); MessageBox.Show(settingsForm.GrandPrixUrl + Environment.NewLine + settingsForm.GrandPrixName + Environment.NewLine + settingsForm.GrandPrixYear); } } }","title":"Form1.cs"},{"location":"Code/Reader.html","text":"Reader.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : Reader.cs /// Brief : Class used to Read the config file for the OCR /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; using System.Windows.Forms; using System.IO; using System.Text.Json; namespace Test_Merge { public class Reader { const int NUMBER_OF_DRIVERS = 20; public List Drivers; public List MainZones; public Reader(string configFile, Bitmap image,bool loadOCR = true) { MainZones = Load(image,configFile,ref Drivers,loadOCR); } /// /// Method that reads the JSON config file and create all the Zones and Windows /// /// The image #id on wich you want to create the zones on public static List Load(Bitmap image,string configFilePath,ref List driverListToFill,bool LoadOCR) { List mainZones = new List(); Bitmap fullImage = image; List drivers; Zone mainZone; try { using (var streamReader = new StreamReader(configFilePath)) { var jsonText = streamReader.ReadToEnd(); var jsonDocument = JsonDocument.Parse(jsonText); var driversNames = jsonDocument.RootElement.GetProperty(\"Drivers\"); driverListToFill = new List(); foreach (var nameElement in driversNames.EnumerateArray()) { driverListToFill.Add(nameElement.GetString()); } var mainProperty = jsonDocument.RootElement.GetProperty(\"Main\"); Point MainPosition = new Point(mainProperty.GetProperty(\"x\").GetInt32(), mainProperty.GetProperty(\"y\").GetInt32()); Size MainSize = new Size(mainProperty.GetProperty(\"width\").GetInt32(), mainProperty.GetProperty(\"height\").GetInt32()); Rectangle MainRectangle = new Rectangle(MainPosition, MainSize); mainZone = new Zone(image, MainRectangle,\"Main\"); var zones = mainProperty.GetProperty(\"Zones\"); var driverZone = zones[0].GetProperty(\"DriverZone\"); Point FirstZonePosition = new Point(driverZone.GetProperty(\"x\").GetInt32(), driverZone.GetProperty(\"y\").GetInt32()); Size FirstZoneSize = new Size(driverZone.GetProperty(\"width\").GetInt32(), driverZone.GetProperty(\"height\").GetInt32()); var windows = driverZone.GetProperty(\"Windows\"); var driverPosition = windows[0].GetProperty(\"Position\"); Size driverPositionArea = new Size(driverPosition.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverPositionPosition = new Point(driverPosition.GetProperty(\"x\").GetInt32(), driverPosition.GetProperty(\"y\").GetInt32()); var driverGapToLeader = windows[0].GetProperty(\"GapToLeader\"); Size driverGapToLeaderArea = new Size(driverGapToLeader.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverGapToLeaderPosition = new Point(driverGapToLeader.GetProperty(\"x\").GetInt32(), driverGapToLeader.GetProperty(\"y\").GetInt32()); var driverLapTime = windows[0].GetProperty(\"LapTime\"); Size driverLapTimeArea = new Size(driverLapTime.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverLapTimePosition = new Point(driverLapTime.GetProperty(\"x\").GetInt32(), driverLapTime.GetProperty(\"y\").GetInt32()); var driverDrs = windows[0].GetProperty(\"DRS\"); Size driverDrsArea = new Size(driverDrs.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverDrsPosition = new Point(driverDrs.GetProperty(\"x\").GetInt32(), driverDrs.GetProperty(\"y\").GetInt32()); var driverTyres = windows[0].GetProperty(\"Tyres\"); Size driverTyresArea = new Size(driverTyres.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverTyresPosition = new Point(driverTyres.GetProperty(\"x\").GetInt32(), driverTyres.GetProperty(\"y\").GetInt32()); var driverName = windows[0].GetProperty(\"Name\"); Size driverNameArea = new Size(driverName.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverNamePosition = new Point(driverName.GetProperty(\"x\").GetInt32(), driverName.GetProperty(\"y\").GetInt32()); var driverSector1 = windows[0].GetProperty(\"Sector1\"); Size driverSector1Area = new Size(driverSector1.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverSector1Position = new Point(driverSector1.GetProperty(\"x\").GetInt32(), driverSector1.GetProperty(\"y\").GetInt32()); var driverSector2 = windows[0].GetProperty(\"Sector2\"); Size driverSector2Area = new Size(driverSector2.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverSector2Position = new Point(driverSector2.GetProperty(\"x\").GetInt32(), driverSector2.GetProperty(\"y\").GetInt32()); var driverSector3 = windows[0].GetProperty(\"Sector3\"); Size driverSector3Area = new Size(driverSector3.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverSector3Position = new Point(driverSector3.GetProperty(\"x\").GetInt32(), driverSector3.GetProperty(\"y\").GetInt32()); float offset = (((float)mainZone.ZoneImage.Height - (float)(driverListToFill.Count * FirstZoneSize.Height)) / (float)driverListToFill.Count); Bitmap MainZoneImage = mainZone.ZoneImage; List zonesToAdd = new List(); List zonesImages = new List(); for (int i = 0; i < NUMBER_OF_DRIVERS; i++) { Point tmpPos = new Point(0, FirstZonePosition.Y + i * FirstZoneSize.Height - Convert.ToInt32(i * offset)); Zone newDriverZone = new Zone(MainZoneImage, new Rectangle(tmpPos, FirstZoneSize), \"DriverZone\"); zonesToAdd.Add(newDriverZone); zonesImages.Add(newDriverZone.ZoneImage); newDriverZone.ZoneImage.Save(\"Driver\"+i+\".png\"); } //Parallel.For(0, NUMBER_OF_DRIVERS, i => for (int i = 0; i < NUMBER_OF_DRIVERS; i++) { Zone newDriverZone = zonesToAdd[(int)i]; Bitmap zoneImg = zonesImages[(int)i]; newDriverZone.AddWindow(new DriverPositionWindow(zoneImg, new Rectangle(driverPositionPosition, driverPositionArea),LoadOCR)); newDriverZone.AddWindow(new DriverGapToLeaderWindow(zoneImg, new Rectangle(driverGapToLeaderPosition, driverGapToLeaderArea), LoadOCR)); newDriverZone.AddWindow(new DriverLapTimeWindow(zoneImg, new Rectangle(driverLapTimePosition, driverLapTimeArea), LoadOCR)); newDriverZone.AddWindow(new DriverDrsWindow(zoneImg, new Rectangle(driverDrsPosition, driverDrsArea), LoadOCR)); newDriverZone.AddWindow(new DriverTyresWindow(zoneImg, new Rectangle(driverTyresPosition, driverTyresArea), LoadOCR)); newDriverZone.AddWindow(new DriverNameWindow(zoneImg, new Rectangle(driverNamePosition, driverNameArea), LoadOCR)); newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector1Position, driverSector1Area),1, LoadOCR)); newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector2Position, driverSector2Area),2, LoadOCR)); newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector3Position, driverSector3Area),3, LoadOCR)); mainZone.AddZone(newDriverZone); }//); //MessageBox.Show(\"We have a main zone with \" + MainZone.Zones.Count() + \" Driver zones with \" + MainZone.Zones[4].Windows.Count() + \" windows each and we have \" + Drivers.Count + \" drivers\"); mainZones.Add(mainZone); } } catch (IOException ex) { MessageBox.Show(\"Error reading JSON file: \" + ex.Message); } catch (JsonException ex) { MessageBox.Show(\"Invalid JSON format: \" + ex.Message); } return mainZones; } /// /// Method that calls all the zones and windows to get the content they can find on the image to display them /// /// The id of the image we are working with /// a string representation of all the returns public async Task Decode(List mainZones,List drivers) { string result = \"\"; List mainResults = new List(); //Decode for (int mainZoneId = 0; mainZoneId < mainZones.Count; mainZoneId++) { switch (mainZoneId) { case 0: //Main Zone foreach (Zone z in mainZones[mainZoneId].Zones) { mainResults.Add(await z.Decode(Drivers)); } break; //Next there could be a Title Zone and TrackInfoZone } } //Display foreach (DriverData driver in mainResults) { result += driver.ToString(); result += Environment.NewLine; } return result; } /// /// Method that can be used to convert an amount of miliseconds into a more readable human form /// /// The given amount of miliseconds ton convert /// A human readable string that represents the ms public static string ConvertMsToTime(int amountOfMs) { //Convert.ToInt32 would round upand I dont want that int minuts = (int)((float)amountOfMs / (1000f * 60f)); int seconds = (int)((amountOfMs - (minuts * 60f * 1000f)) / 1000); int ms = amountOfMs - ((minuts * 60 * 1000) + (seconds * 1000)); return minuts + \":\" + seconds.ToString(\"00\") + \":\" + ms.ToString(\"000\"); } /// /// Old method that can draw on an image where the windows and zones are created. mostly used for debugging /// /// the #id of the image we are working with /// the drawed bitmap public Bitmap Draw(Bitmap image,List mainZones) { Graphics g = Graphics.FromImage(image); foreach (Zone z in mainZones) { int count = 0; foreach (Zone zz in z.Zones) { g.DrawRectangle(Pens.Red, z.Bounds); foreach (Window w in zz.Windows) { g.DrawRectangle(Pens.Blue, new Rectangle(z.Bounds.X + zz.Bounds.X, z.Bounds.Y + zz.Bounds.Y, zz.Bounds.Width, zz.Bounds.Height)); } count++; } } return image; } } }","title":"Reader.cs"},{"location":"Code/Reader.html#readercs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : Reader.cs /// Brief : Class used to Read the config file for the OCR /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; using System.Windows.Forms; using System.IO; using System.Text.Json; namespace Test_Merge { public class Reader { const int NUMBER_OF_DRIVERS = 20; public List Drivers; public List MainZones; public Reader(string configFile, Bitmap image,bool loadOCR = true) { MainZones = Load(image,configFile,ref Drivers,loadOCR); } /// /// Method that reads the JSON config file and create all the Zones and Windows /// /// The image #id on wich you want to create the zones on public static List Load(Bitmap image,string configFilePath,ref List driverListToFill,bool LoadOCR) { List mainZones = new List(); Bitmap fullImage = image; List drivers; Zone mainZone; try { using (var streamReader = new StreamReader(configFilePath)) { var jsonText = streamReader.ReadToEnd(); var jsonDocument = JsonDocument.Parse(jsonText); var driversNames = jsonDocument.RootElement.GetProperty(\"Drivers\"); driverListToFill = new List(); foreach (var nameElement in driversNames.EnumerateArray()) { driverListToFill.Add(nameElement.GetString()); } var mainProperty = jsonDocument.RootElement.GetProperty(\"Main\"); Point MainPosition = new Point(mainProperty.GetProperty(\"x\").GetInt32(), mainProperty.GetProperty(\"y\").GetInt32()); Size MainSize = new Size(mainProperty.GetProperty(\"width\").GetInt32(), mainProperty.GetProperty(\"height\").GetInt32()); Rectangle MainRectangle = new Rectangle(MainPosition, MainSize); mainZone = new Zone(image, MainRectangle,\"Main\"); var zones = mainProperty.GetProperty(\"Zones\"); var driverZone = zones[0].GetProperty(\"DriverZone\"); Point FirstZonePosition = new Point(driverZone.GetProperty(\"x\").GetInt32(), driverZone.GetProperty(\"y\").GetInt32()); Size FirstZoneSize = new Size(driverZone.GetProperty(\"width\").GetInt32(), driverZone.GetProperty(\"height\").GetInt32()); var windows = driverZone.GetProperty(\"Windows\"); var driverPosition = windows[0].GetProperty(\"Position\"); Size driverPositionArea = new Size(driverPosition.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverPositionPosition = new Point(driverPosition.GetProperty(\"x\").GetInt32(), driverPosition.GetProperty(\"y\").GetInt32()); var driverGapToLeader = windows[0].GetProperty(\"GapToLeader\"); Size driverGapToLeaderArea = new Size(driverGapToLeader.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverGapToLeaderPosition = new Point(driverGapToLeader.GetProperty(\"x\").GetInt32(), driverGapToLeader.GetProperty(\"y\").GetInt32()); var driverLapTime = windows[0].GetProperty(\"LapTime\"); Size driverLapTimeArea = new Size(driverLapTime.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverLapTimePosition = new Point(driverLapTime.GetProperty(\"x\").GetInt32(), driverLapTime.GetProperty(\"y\").GetInt32()); var driverDrs = windows[0].GetProperty(\"DRS\"); Size driverDrsArea = new Size(driverDrs.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverDrsPosition = new Point(driverDrs.GetProperty(\"x\").GetInt32(), driverDrs.GetProperty(\"y\").GetInt32()); var driverTyres = windows[0].GetProperty(\"Tyres\"); Size driverTyresArea = new Size(driverTyres.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverTyresPosition = new Point(driverTyres.GetProperty(\"x\").GetInt32(), driverTyres.GetProperty(\"y\").GetInt32()); var driverName = windows[0].GetProperty(\"Name\"); Size driverNameArea = new Size(driverName.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverNamePosition = new Point(driverName.GetProperty(\"x\").GetInt32(), driverName.GetProperty(\"y\").GetInt32()); var driverSector1 = windows[0].GetProperty(\"Sector1\"); Size driverSector1Area = new Size(driverSector1.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverSector1Position = new Point(driverSector1.GetProperty(\"x\").GetInt32(), driverSector1.GetProperty(\"y\").GetInt32()); var driverSector2 = windows[0].GetProperty(\"Sector2\"); Size driverSector2Area = new Size(driverSector2.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverSector2Position = new Point(driverSector2.GetProperty(\"x\").GetInt32(), driverSector2.GetProperty(\"y\").GetInt32()); var driverSector3 = windows[0].GetProperty(\"Sector3\"); Size driverSector3Area = new Size(driverSector3.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverSector3Position = new Point(driverSector3.GetProperty(\"x\").GetInt32(), driverSector3.GetProperty(\"y\").GetInt32()); float offset = (((float)mainZone.ZoneImage.Height - (float)(driverListToFill.Count * FirstZoneSize.Height)) / (float)driverListToFill.Count); Bitmap MainZoneImage = mainZone.ZoneImage; List zonesToAdd = new List(); List zonesImages = new List(); for (int i = 0; i < NUMBER_OF_DRIVERS; i++) { Point tmpPos = new Point(0, FirstZonePosition.Y + i * FirstZoneSize.Height - Convert.ToInt32(i * offset)); Zone newDriverZone = new Zone(MainZoneImage, new Rectangle(tmpPos, FirstZoneSize), \"DriverZone\"); zonesToAdd.Add(newDriverZone); zonesImages.Add(newDriverZone.ZoneImage); newDriverZone.ZoneImage.Save(\"Driver\"+i+\".png\"); } //Parallel.For(0, NUMBER_OF_DRIVERS, i => for (int i = 0; i < NUMBER_OF_DRIVERS; i++) { Zone newDriverZone = zonesToAdd[(int)i]; Bitmap zoneImg = zonesImages[(int)i]; newDriverZone.AddWindow(new DriverPositionWindow(zoneImg, new Rectangle(driverPositionPosition, driverPositionArea),LoadOCR)); newDriverZone.AddWindow(new DriverGapToLeaderWindow(zoneImg, new Rectangle(driverGapToLeaderPosition, driverGapToLeaderArea), LoadOCR)); newDriverZone.AddWindow(new DriverLapTimeWindow(zoneImg, new Rectangle(driverLapTimePosition, driverLapTimeArea), LoadOCR)); newDriverZone.AddWindow(new DriverDrsWindow(zoneImg, new Rectangle(driverDrsPosition, driverDrsArea), LoadOCR)); newDriverZone.AddWindow(new DriverTyresWindow(zoneImg, new Rectangle(driverTyresPosition, driverTyresArea), LoadOCR)); newDriverZone.AddWindow(new DriverNameWindow(zoneImg, new Rectangle(driverNamePosition, driverNameArea), LoadOCR)); newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector1Position, driverSector1Area),1, LoadOCR)); newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector2Position, driverSector2Area),2, LoadOCR)); newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector3Position, driverSector3Area),3, LoadOCR)); mainZone.AddZone(newDriverZone); }//); //MessageBox.Show(\"We have a main zone with \" + MainZone.Zones.Count() + \" Driver zones with \" + MainZone.Zones[4].Windows.Count() + \" windows each and we have \" + Drivers.Count + \" drivers\"); mainZones.Add(mainZone); } } catch (IOException ex) { MessageBox.Show(\"Error reading JSON file: \" + ex.Message); } catch (JsonException ex) { MessageBox.Show(\"Invalid JSON format: \" + ex.Message); } return mainZones; } /// /// Method that calls all the zones and windows to get the content they can find on the image to display them /// /// The id of the image we are working with /// a string representation of all the returns public async Task Decode(List mainZones,List drivers) { string result = \"\"; List mainResults = new List(); //Decode for (int mainZoneId = 0; mainZoneId < mainZones.Count; mainZoneId++) { switch (mainZoneId) { case 0: //Main Zone foreach (Zone z in mainZones[mainZoneId].Zones) { mainResults.Add(await z.Decode(Drivers)); } break; //Next there could be a Title Zone and TrackInfoZone } } //Display foreach (DriverData driver in mainResults) { result += driver.ToString(); result += Environment.NewLine; } return result; } /// /// Method that can be used to convert an amount of miliseconds into a more readable human form /// /// The given amount of miliseconds ton convert /// A human readable string that represents the ms public static string ConvertMsToTime(int amountOfMs) { //Convert.ToInt32 would round upand I dont want that int minuts = (int)((float)amountOfMs / (1000f * 60f)); int seconds = (int)((amountOfMs - (minuts * 60f * 1000f)) / 1000); int ms = amountOfMs - ((minuts * 60 * 1000) + (seconds * 1000)); return minuts + \":\" + seconds.ToString(\"00\") + \":\" + ms.ToString(\"000\"); } /// /// Old method that can draw on an image where the windows and zones are created. mostly used for debugging /// /// the #id of the image we are working with /// the drawed bitmap public Bitmap Draw(Bitmap image,List mainZones) { Graphics g = Graphics.FromImage(image); foreach (Zone z in mainZones) { int count = 0; foreach (Zone zz in z.Zones) { g.DrawRectangle(Pens.Red, z.Bounds); foreach (Window w in zz.Windows) { g.DrawRectangle(Pens.Blue, new Rectangle(z.Bounds.X + zz.Bounds.X, z.Bounds.Y + zz.Bounds.Y, zz.Bounds.Width, zz.Bounds.Height)); } count++; } } return image; } } }","title":"Reader.cs"},{"location":"Code/Zone.html","text":"Zone.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/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 : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Test_Merge { public class Zone { private Rectangle _bounds; private List _zones; private List _windows; private Bitmap _image; private string _name; public Bitmap ZoneImage { get { //This little trickery lets you have the image that the zone sees Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); return sample; } } public Bitmap Image { get { return _image; } set { //It automatically sets the image for the contained windows and zones _image = Image; foreach (Window w in Windows) { w.Image = ZoneImage; } foreach (Zone z in Zones) { z.Image = Image; } } } public Rectangle Bounds { get => _bounds; protected set => _bounds = value; } public List Zones { get => _zones; protected set => _zones = value; } public List Windows { get => _windows; protected set => _windows = value; } public string Name { get => _name; protected set => _name = value; } public Zone(Bitmap image, Rectangle bounds, string name) { Windows = new List(); Zones = new List(); Name = name; //You cant set the image in the CTOR because the processing is impossible at first initiation _image = image; Bounds = bounds; } /// /// Adds a zone to the list of zones /// /// The zone you want to add public virtual void AddZone(Zone zone) { Zones.Add(zone); } /// /// Add a window to the list of windows /// /// the window you want to add public virtual void AddWindow(Window window) { Windows.Add(window); } /// /// Calls all the windows to do OCR and to give back the results so we can send them to the model /// /// A list of all the driver in the race to help with text recognition /// A driver data object that contains all the infos about a driver public virtual async Task Decode(List driverList) { int sectorCount = 0; DriverData result = new DriverData(); Parallel.ForEach(Windows, async w => { // A switch would be prettier but I dont think its supported in this C# version if (w is DriverNameWindow) result.Name = (string)await (w as DriverNameWindow).DecodePng(driverList); if (w is DriverDrsWindow) result.DRS = (bool)await (w as DriverDrsWindow).DecodePng(); if (w is DriverGapToLeaderWindow) result.GapToLeader = (int)await (w as DriverGapToLeaderWindow).DecodePng(); if (w is DriverLapTimeWindow) result.LapTime = (int)await (w as DriverLapTimeWindow).DecodePng(); if (w is DriverPositionWindow) result.Position = (int)await (w as DriverPositionWindow).DecodePng(); if (w is DriverSectorWindow) { sectorCount++; if (sectorCount == 1) result.Sector1 = (int)await (w as DriverSectorWindow).DecodePng(); if (sectorCount == 2) result.Sector2 = (int)await (w as DriverSectorWindow).DecodePng(); if (sectorCount == 3) result.Sector3 = (int)await (w as DriverSectorWindow).DecodePng(); } if (w is DriverTyresWindow) result.CurrentTyre = (Tyre)await (w as DriverTyresWindow).DecodePng(); }); return result; } public virtual Bitmap Draw() { Bitmap img; //If its the main zone we want to see everything if (Zones.Count > 0) { img = Image; } else { img = ZoneImage; } Graphics g = Graphics.FromImage(img); //If its the main zone we need to visualize the Zone bounds displayed if (Zones.Count > 0) g.DrawRectangle(new Pen(Brushes.Violet, 5), Bounds); foreach (Zone z in Zones) { Rectangle newBounds = new Rectangle(z.Bounds.X, z.Bounds.Y + Bounds.Y, z.Bounds.Width, z.Bounds.Height); g.DrawRectangle(Pens.Red, newBounds); } foreach (Window w in Windows) { g.DrawRectangle(Pens.Blue, w.Bounds); } return img; } public void ResetZones() { Zones.Clear(); } public void ResetWindows() { foreach (Zone z in Zones) { z.ResetWindows(); } Windows.Clear(); } public virtual string ToJSON() { string result = \"\"; result += \"\\\"\" + Name + \"\\\":{\" + Environment.NewLine; result += \"\\t\" + \"\\\"x\\\":\" + Bounds.X + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"y\\\":\" + Bounds.Y + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"width\\\":\" + Bounds.Width + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"height\\\":\" + Bounds.Height; if (Windows.Count != 0) { result += \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"Windows\\\":[\" + Environment.NewLine; result += \"\\t\\t{\" + Environment.NewLine; int Wcount = 0; foreach (Window w in Windows) { result += \"\\t\\t\" + w.ToJSON(); Wcount++; if (Wcount != Windows.Count) result += \",\"; } result += \"\\t\\t}\" + Environment.NewLine; result += \"\\t\" + \"]\" + Environment.NewLine; } else { result += Environment.NewLine; } if (Zones.Count != 0) { result += \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"Zones\\\":[\" + Environment.NewLine; result += \"\\t\\t{\" + Environment.NewLine; int Zcount = 0; //foreach (Zone z in Zones) //{ result += \"\\t\\t\" + Zones[0].ToJSON(); Zcount++; if (Zcount != Zones.Count) //result += \",\"; //} result += \"\\t\\t}\" + Environment.NewLine; result += \"\\t\" + \"]\" + Environment.NewLine; } else { result += Environment.NewLine; } result += \"}\"; return result; } /// /// Checks if the given Rectangle fits in the current zone /// /// The Rectangle you want to check the fittment /// protected bool Fits(Rectangle inputRectangle) { if (inputRectangle.X + inputRectangle.Width > Bounds.Width || inputRectangle.Y + inputRectangle.Height > Bounds.Height || inputRectangle.X < 0 || inputRectangle.Y < 0) { return false; } else { return true; } } } }","title":"Zone.cs"},{"location":"Code/Zone.html#zonecs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/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 : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Test_Merge { public class Zone { private Rectangle _bounds; private List _zones; private List _windows; private Bitmap _image; private string _name; public Bitmap ZoneImage { get { //This little trickery lets you have the image that the zone sees Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); return sample; } } public Bitmap Image { get { return _image; } set { //It automatically sets the image for the contained windows and zones _image = Image; foreach (Window w in Windows) { w.Image = ZoneImage; } foreach (Zone z in Zones) { z.Image = Image; } } } public Rectangle Bounds { get => _bounds; protected set => _bounds = value; } public List Zones { get => _zones; protected set => _zones = value; } public List Windows { get => _windows; protected set => _windows = value; } public string Name { get => _name; protected set => _name = value; } public Zone(Bitmap image, Rectangle bounds, string name) { Windows = new List(); Zones = new List(); Name = name; //You cant set the image in the CTOR because the processing is impossible at first initiation _image = image; Bounds = bounds; } /// /// Adds a zone to the list of zones /// /// The zone you want to add public virtual void AddZone(Zone zone) { Zones.Add(zone); } /// /// Add a window to the list of windows /// /// the window you want to add public virtual void AddWindow(Window window) { Windows.Add(window); } /// /// Calls all the windows to do OCR and to give back the results so we can send them to the model /// /// A list of all the driver in the race to help with text recognition /// A driver data object that contains all the infos about a driver public virtual async Task Decode(List driverList) { int sectorCount = 0; DriverData result = new DriverData(); Parallel.ForEach(Windows, async w => { // A switch would be prettier but I dont think its supported in this C# version if (w is DriverNameWindow) result.Name = (string)await (w as DriverNameWindow).DecodePng(driverList); if (w is DriverDrsWindow) result.DRS = (bool)await (w as DriverDrsWindow).DecodePng(); if (w is DriverGapToLeaderWindow) result.GapToLeader = (int)await (w as DriverGapToLeaderWindow).DecodePng(); if (w is DriverLapTimeWindow) result.LapTime = (int)await (w as DriverLapTimeWindow).DecodePng(); if (w is DriverPositionWindow) result.Position = (int)await (w as DriverPositionWindow).DecodePng(); if (w is DriverSectorWindow) { sectorCount++; if (sectorCount == 1) result.Sector1 = (int)await (w as DriverSectorWindow).DecodePng(); if (sectorCount == 2) result.Sector2 = (int)await (w as DriverSectorWindow).DecodePng(); if (sectorCount == 3) result.Sector3 = (int)await (w as DriverSectorWindow).DecodePng(); } if (w is DriverTyresWindow) result.CurrentTyre = (Tyre)await (w as DriverTyresWindow).DecodePng(); }); return result; } public virtual Bitmap Draw() { Bitmap img; //If its the main zone we want to see everything if (Zones.Count > 0) { img = Image; } else { img = ZoneImage; } Graphics g = Graphics.FromImage(img); //If its the main zone we need to visualize the Zone bounds displayed if (Zones.Count > 0) g.DrawRectangle(new Pen(Brushes.Violet, 5), Bounds); foreach (Zone z in Zones) { Rectangle newBounds = new Rectangle(z.Bounds.X, z.Bounds.Y + Bounds.Y, z.Bounds.Width, z.Bounds.Height); g.DrawRectangle(Pens.Red, newBounds); } foreach (Window w in Windows) { g.DrawRectangle(Pens.Blue, w.Bounds); } return img; } public void ResetZones() { Zones.Clear(); } public void ResetWindows() { foreach (Zone z in Zones) { z.ResetWindows(); } Windows.Clear(); } public virtual string ToJSON() { string result = \"\"; result += \"\\\"\" + Name + \"\\\":{\" + Environment.NewLine; result += \"\\t\" + \"\\\"x\\\":\" + Bounds.X + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"y\\\":\" + Bounds.Y + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"width\\\":\" + Bounds.Width + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"height\\\":\" + Bounds.Height; if (Windows.Count != 0) { result += \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"Windows\\\":[\" + Environment.NewLine; result += \"\\t\\t{\" + Environment.NewLine; int Wcount = 0; foreach (Window w in Windows) { result += \"\\t\\t\" + w.ToJSON(); Wcount++; if (Wcount != Windows.Count) result += \",\"; } result += \"\\t\\t}\" + Environment.NewLine; result += \"\\t\" + \"]\" + Environment.NewLine; } else { result += Environment.NewLine; } if (Zones.Count != 0) { result += \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"Zones\\\":[\" + Environment.NewLine; result += \"\\t\\t{\" + Environment.NewLine; int Zcount = 0; //foreach (Zone z in Zones) //{ result += \"\\t\\t\" + Zones[0].ToJSON(); Zcount++; if (Zcount != Zones.Count) //result += \",\"; //} result += \"\\t\\t}\" + Environment.NewLine; result += \"\\t\" + \"]\" + Environment.NewLine; } else { result += Environment.NewLine; } result += \"}\"; return result; } /// /// Checks if the given Rectangle fits in the current zone /// /// The Rectangle you want to check the fittment /// protected bool Fits(Rectangle inputRectangle) { if (inputRectangle.X + inputRectangle.Width > Bounds.Width || inputRectangle.Y + inputRectangle.Height > Bounds.Height || inputRectangle.X < 0 || inputRectangle.Y < 0) { return false; } else { return true; } } } }","title":"Zone.cs"},{"location":"Code/DriverDrsWindow.html","text":"DriverDrsWindow.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverDrsWindow.cs /// Brief : Window containing DRS related method and infos /// Version : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.Linq; using System.Text; using System.Threading.Tasks; using Tesseract; namespace Test_Merge { internal class DriverDrsWindow:Window { private static int EmptyDrsGreenValue = -1; private static Random rnd = new Random(); public DriverDrsWindow(Bitmap image, Rectangle bounds,bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"DRS\"; } public override async Task DecodePng() { bool result = false; int greenValue = GetGreenPixels(); if (EmptyDrsGreenValue == -1) EmptyDrsGreenValue = greenValue; if (greenValue > EmptyDrsGreenValue + EmptyDrsGreenValue / 100 * 30) result = true; return result; } private unsafe int GetGreenPixels() { int tot = 0; Bitmap bmp = WindowImage; Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height); BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, bmp.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); for (int y = 0; y < bmp.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < bmp.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); byte blue = pixel[0]; byte green = pixel[1]; byte red = pixel[2]; if (green > blue * 1.5 && green > red * 1.5) { tot++; } } } } bmp.UnlockBits(bmpData); return tot; } public Rectangle GetBox() { var tessImage = Pix.LoadFromMemory(ImageToByte(WindowImage)); Engine.SetVariable(\"tessedit_char_whitelist\", \"\"); Page page = Engine.Process(tessImage); using (var iter = page.GetIterator()) { iter.Begin(); do { Rect boundingBox; // Get the bounding box for the current element if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox)) { page.Dispose(); return new Rectangle(boundingBox.X1, boundingBox.X2, boundingBox.Width, boundingBox.Height); } } while (iter.Next(PageIteratorLevel.Word)); page.Dispose(); return new Rectangle(0, 0, 0, 0); } } } }","title":"DriverDrsWindow.cs"},{"location":"Code/DriverDrsWindow.html#driverdrswindowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverDrsWindow.cs /// Brief : Window containing DRS related method and infos /// Version : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.Linq; using System.Text; using System.Threading.Tasks; using Tesseract; namespace Test_Merge { internal class DriverDrsWindow:Window { private static int EmptyDrsGreenValue = -1; private static Random rnd = new Random(); public DriverDrsWindow(Bitmap image, Rectangle bounds,bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"DRS\"; } public override async Task DecodePng() { bool result = false; int greenValue = GetGreenPixels(); if (EmptyDrsGreenValue == -1) EmptyDrsGreenValue = greenValue; if (greenValue > EmptyDrsGreenValue + EmptyDrsGreenValue / 100 * 30) result = true; return result; } private unsafe int GetGreenPixels() { int tot = 0; Bitmap bmp = WindowImage; Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height); BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, bmp.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); for (int y = 0; y < bmp.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < bmp.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); byte blue = pixel[0]; byte green = pixel[1]; byte red = pixel[2]; if (green > blue * 1.5 && green > red * 1.5) { tot++; } } } } bmp.UnlockBits(bmpData); return tot; } public Rectangle GetBox() { var tessImage = Pix.LoadFromMemory(ImageToByte(WindowImage)); Engine.SetVariable(\"tessedit_char_whitelist\", \"\"); Page page = Engine.Process(tessImage); using (var iter = page.GetIterator()) { iter.Begin(); do { Rect boundingBox; // Get the bounding box for the current element if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox)) { page.Dispose(); return new Rectangle(boundingBox.X1, boundingBox.X2, boundingBox.Width, boundingBox.Height); } } while (iter.Next(PageIteratorLevel.Word)); page.Dispose(); return new Rectangle(0, 0, 0, 0); } } } }","title":"DriverDrsWindow.cs"},{"location":"Code/DriverNameWindow.html","text":"DriverNameWindow.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverNameWindow /// Brief : Window containing infos about the name of the driver /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { public class DriverNameWindow : Window { public static Random rnd = new Random(); public DriverNameWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Name\"; } /// /// Decodes using OCR wich driver name is in the image /// /// /// The driver name in string public override async Task DecodePng(List DriverList) { string result = \"\"; result = await GetStringFromPng(WindowImage, Engine); if (!IsADriver(DriverList, result)) { //I put everything in uppercase to try to lower the chances of bad answers result = FindClosestMatch(DriverList.ConvertAll(d => d.ToUpper()), result.ToUpper()); } return result; } /// /// Verifies that the name found in the OCR is a valid name /// /// /// /// If ye or no the driver exists private static bool IsADriver(List driverList, string potentialDriver) { bool result = false; //I cant use drivers.Contains because it has missmatched cases and all foreach (string name in driverList) { if (name.ToUpper() == potentialDriver.ToUpper()) result = true; } return result; } } }","title":"DriverNameWindow.cs"},{"location":"Code/DriverNameWindow.html#drivernamewindowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverNameWindow /// Brief : Window containing infos about the name of the driver /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { public class DriverNameWindow : Window { public static Random rnd = new Random(); public DriverNameWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Name\"; } /// /// Decodes using OCR wich driver name is in the image /// /// /// The driver name in string public override async Task DecodePng(List DriverList) { string result = \"\"; result = await GetStringFromPng(WindowImage, Engine); if (!IsADriver(DriverList, result)) { //I put everything in uppercase to try to lower the chances of bad answers result = FindClosestMatch(DriverList.ConvertAll(d => d.ToUpper()), result.ToUpper()); } return result; } /// /// Verifies that the name found in the OCR is a valid name /// /// /// /// If ye or no the driver exists private static bool IsADriver(List driverList, string potentialDriver) { bool result = false; //I cant use drivers.Contains because it has missmatched cases and all foreach (string name in driverList) { if (name.ToUpper() == potentialDriver.ToUpper()) result = true; } return result; } } }","title":"DriverNameWindow.cs"},{"location":"Code/DriverTyresWindow.html","text":"DriverTyresWindow.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverTyresWindow.cs /// Brief : Window containing infos about a driver's tyre /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { public class DriverTyresWindow:Window { private static Random rnd = new Random(); int seed = rnd.Next(0, 10000); //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); public static Color HARD_TYRE_COLOR = Color.FromArgb(0xa4, 0xa5, 0xa8); public static Color INTER_TYRE_COLOR = Color.FromArgb(0x00, 0xa4, 0x2e); public static Color WET_TYRE_COLOR = Color.FromArgb(0x27, 0x60, 0xa6); public static Color EMPTY_COLOR = Color.FromArgb(0x20, 0x20, 0x20); public DriverTyresWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Tyres\"; } /// /// This will decode the content of the image /// /// And object containing what was on the image public override async Task DecodePng() { return await GetTyreInfos(); } /// /// Method that will decode whats on the image and return the tyre infos it could manage to recover /// /// A tyre object containing tyre infos private async Task GetTyreInfos() { Bitmap tyreZone = GetSmallBitmapFromBigOne(WindowImage, FindTyreZone()); Tyre.Type type = Tyre.Type.Undefined; type = GetTyreTypeFromColor(OcrImage.GetAvgColorFromBitmap(tyreZone)); int laps = -1; string number = await GetStringFromPng(tyreZone, Engine, \"0123456789\", OcrImage.WindowType.Tyre); try { laps = Convert.ToInt32(number); } catch { //We could not convert the number so its a letter so its 0 laps old laps = 0; } //tyreZone.Save(Reader.DEBUG_DUMP_FOLDER + \"Tyre\" + type + \"Laps\" + laps + '#' + rnd.Next(0, 1000) + \".png\"); return new Tyre(type, laps); } /// /// Finds where the important part of the image is /// /// A rectangle containing position and dimensions of the important part of the image private Rectangle FindTyreZone() { Bitmap bmp = WindowImage; int currentPosition = bmp.Width; int height = bmp.Height / 2; Color limitColor = Color.FromArgb(0x50, 0x50, 0x50); Color currentColor = Color.FromArgb(0, 0, 0); Size newWindowSize = new Size(bmp.Height - Convert.ToInt32((float)bmp.Height / 100f * 25f), bmp.Height - Convert.ToInt32((float)bmp.Height / 100f * 35f)); while (currentColor.R <= limitColor.R && currentColor.G <= limitColor.G && currentColor.B <= limitColor.B && currentPosition > 0) { currentPosition--; currentColor = bmp.GetPixel(currentPosition, height); } //Its here to let the new window include a little bit of the right int CorrectedX = currentPosition - (newWindowSize.Width) + Convert.ToInt32((float)newWindowSize.Width / 100f * 10f); int CorrectedY = Convert.ToInt32((float)newWindowSize.Height / 100f * 35f); if (CorrectedX <= 0) return new Rectangle(0, 0, newWindowSize.Width, newWindowSize.Height); return new Rectangle(CorrectedX, CorrectedY, newWindowSize.Width, newWindowSize.Height); } //This method has been created with the help of chatGPT /// /// Methods that compares a list of colors to see wich is the closest from the input color and decide wich tyre type it is /// /// The color that you found /// The tyre type public Tyre.Type GetTyreTypeFromColor(Color inputColor) { Tyre.Type type = Tyre.Type.Undefined; List colors = new List(); //dont forget that if for some reason someday F1 adds a new Tyre type you will need to add it in the constants but also here in the list //You will also need to add it below in the Tyre object's enum and add an if in the end of this method colors.Add(SOFT_TYRE_COLOR); colors.Add(MEDIUM_TYRE_COLOR); colors.Add(HARD_TYRE_COLOR); colors.Add(INTER_TYRE_COLOR); colors.Add(WET_TYRE_COLOR); colors.Add(EMPTY_COLOR); Color closestColor = colors[0]; int closestDistance = int.MaxValue; foreach (Color color in colors) { int distance = Math.Abs(color.R - inputColor.R) + Math.Abs(color.G - inputColor.G) + Math.Abs(color.B - inputColor.B); if (distance < closestDistance) { closestColor = color; closestDistance = distance; } } //We cant use a switch as the colors cant be constants ... if (closestColor == SOFT_TYRE_COLOR) type = Tyre.Type.Soft; if (closestColor == MEDIUM_TYRE_COLOR) type = Tyre.Type.Medium; if (closestColor == HARD_TYRE_COLOR) type = Tyre.Type.Hard; if (closestColor == INTER_TYRE_COLOR) type = Tyre.Type.Inter; if (closestColor == WET_TYRE_COLOR) type = Tyre.Type.Wet; if (closestColor == EMPTY_COLOR) return Tyre.Type.Undefined; return type; } } }","title":"DriverTyresWindow.cs"},{"location":"Code/DriverTyresWindow.html#drivertyreswindowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverTyresWindow.cs /// Brief : Window containing infos about a driver's tyre /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { public class DriverTyresWindow:Window { private static Random rnd = new Random(); int seed = rnd.Next(0, 10000); //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); public static Color HARD_TYRE_COLOR = Color.FromArgb(0xa4, 0xa5, 0xa8); public static Color INTER_TYRE_COLOR = Color.FromArgb(0x00, 0xa4, 0x2e); public static Color WET_TYRE_COLOR = Color.FromArgb(0x27, 0x60, 0xa6); public static Color EMPTY_COLOR = Color.FromArgb(0x20, 0x20, 0x20); public DriverTyresWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Tyres\"; } /// /// This will decode the content of the image /// /// And object containing what was on the image public override async Task DecodePng() { return await GetTyreInfos(); } /// /// Method that will decode whats on the image and return the tyre infos it could manage to recover /// /// A tyre object containing tyre infos private async Task GetTyreInfos() { Bitmap tyreZone = GetSmallBitmapFromBigOne(WindowImage, FindTyreZone()); Tyre.Type type = Tyre.Type.Undefined; type = GetTyreTypeFromColor(OcrImage.GetAvgColorFromBitmap(tyreZone)); int laps = -1; string number = await GetStringFromPng(tyreZone, Engine, \"0123456789\", OcrImage.WindowType.Tyre); try { laps = Convert.ToInt32(number); } catch { //We could not convert the number so its a letter so its 0 laps old laps = 0; } //tyreZone.Save(Reader.DEBUG_DUMP_FOLDER + \"Tyre\" + type + \"Laps\" + laps + '#' + rnd.Next(0, 1000) + \".png\"); return new Tyre(type, laps); } /// /// Finds where the important part of the image is /// /// A rectangle containing position and dimensions of the important part of the image private Rectangle FindTyreZone() { Bitmap bmp = WindowImage; int currentPosition = bmp.Width; int height = bmp.Height / 2; Color limitColor = Color.FromArgb(0x50, 0x50, 0x50); Color currentColor = Color.FromArgb(0, 0, 0); Size newWindowSize = new Size(bmp.Height - Convert.ToInt32((float)bmp.Height / 100f * 25f), bmp.Height - Convert.ToInt32((float)bmp.Height / 100f * 35f)); while (currentColor.R <= limitColor.R && currentColor.G <= limitColor.G && currentColor.B <= limitColor.B && currentPosition > 0) { currentPosition--; currentColor = bmp.GetPixel(currentPosition, height); } //Its here to let the new window include a little bit of the right int CorrectedX = currentPosition - (newWindowSize.Width) + Convert.ToInt32((float)newWindowSize.Width / 100f * 10f); int CorrectedY = Convert.ToInt32((float)newWindowSize.Height / 100f * 35f); if (CorrectedX <= 0) return new Rectangle(0, 0, newWindowSize.Width, newWindowSize.Height); return new Rectangle(CorrectedX, CorrectedY, newWindowSize.Width, newWindowSize.Height); } //This method has been created with the help of chatGPT /// /// Methods that compares a list of colors to see wich is the closest from the input color and decide wich tyre type it is /// /// The color that you found /// The tyre type public Tyre.Type GetTyreTypeFromColor(Color inputColor) { Tyre.Type type = Tyre.Type.Undefined; List colors = new List(); //dont forget that if for some reason someday F1 adds a new Tyre type you will need to add it in the constants but also here in the list //You will also need to add it below in the Tyre object's enum and add an if in the end of this method colors.Add(SOFT_TYRE_COLOR); colors.Add(MEDIUM_TYRE_COLOR); colors.Add(HARD_TYRE_COLOR); colors.Add(INTER_TYRE_COLOR); colors.Add(WET_TYRE_COLOR); colors.Add(EMPTY_COLOR); Color closestColor = colors[0]; int closestDistance = int.MaxValue; foreach (Color color in colors) { int distance = Math.Abs(color.R - inputColor.R) + Math.Abs(color.G - inputColor.G) + Math.Abs(color.B - inputColor.B); if (distance < closestDistance) { closestColor = color; closestDistance = distance; } } //We cant use a switch as the colors cant be constants ... if (closestColor == SOFT_TYRE_COLOR) type = Tyre.Type.Soft; if (closestColor == MEDIUM_TYRE_COLOR) type = Tyre.Type.Medium; if (closestColor == HARD_TYRE_COLOR) type = Tyre.Type.Hard; if (closestColor == INTER_TYRE_COLOR) type = Tyre.Type.Inter; if (closestColor == WET_TYRE_COLOR) type = Tyre.Type.Wet; if (closestColor == EMPTY_COLOR) return Tyre.Type.Undefined; return type; } } }","title":"DriverTyresWindow.cs"},{"location":"Code/OcrImage.html","text":"OcrImage.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : OcrImage.cs /// Brief : Class containing all the methods used to enhance images for OCR /// Version : 0.1 using System; using System.Collections.Generic; using System.Threading.Tasks; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; namespace Test_Merge { public class OcrImage { //this is a hardcoded value based on the colors of the F1TV data channel background you can change it if sometime in the future the color changes //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; public enum WindowType { LapTime, Text, Sector, Gap, Tyre, } /// /// Create a new Ocr image to help enhance the given bitmap for OCR /// /// The image you want to enhance public OcrImage(Bitmap inputBitmap) { InputBitmap = inputBitmap; } /// /// Enhances the image depending on wich type of window the image comes from /// /// The type of the window. Depending on it different enhancing features will be applied /// The enhanced Bitmap public Bitmap Enhance(WindowType type = WindowType.Text) { Bitmap outputBitmap = (Bitmap)InputBitmap.Clone(); switch (type) { case WindowType.LapTime: outputBitmap = Tresholding(outputBitmap, 185); outputBitmap = Resize(outputBitmap, 2); outputBitmap = Dilatation(outputBitmap, 1); outputBitmap = Erode(outputBitmap, 1); break; case WindowType.Text: outputBitmap = InvertColors(outputBitmap); outputBitmap = Tresholding(outputBitmap, 165); outputBitmap = Resize(outputBitmap, 2); outputBitmap = Dilatation(outputBitmap, 1); break; case WindowType.Tyre: outputBitmap = RemoveUseless(outputBitmap); outputBitmap = Resize(outputBitmap, 4); outputBitmap = Dilatation(outputBitmap, 1); break; default: outputBitmap = Tresholding(outputBitmap, 165); outputBitmap = Resize(outputBitmap, 4); outputBitmap = Erode(outputBitmap, 1); break; } return outputBitmap; } /// /// Method that convert a colored RGB bitmap into a GrayScale image /// /// The Bitmap you want to convert /// The bitmap in grayscale 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); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8; unsafe { 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); byte blue = pixel[0]; byte green = pixel[1]; byte red = pixel[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); pixel[0] = pixel[1] = pixel[2] = (byte)gray; } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// /// Method that binaries the input image up to a certain treshold given /// /// the bitmap you want to convert to binary colors /// The floor at wich the color is considered as white or black /// The binarised bitmap public static Bitmap Tresholding(Bitmap inputBitmap, int threshold) { 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 { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); int bmpHeight = inputBitmap.Height; int bmpWidth = inputBitmap.Width; Parallel.For(0, bmpHeight, y => { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < bmpWidth; x++) { byte* pixel = currentLine + (x * bytesPerPixel); byte blue = pixel[0]; byte green = pixel[1]; byte red = pixel[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); int value = gray < threshold ? 0 : 255; pixel[0] = pixel[1] = pixel[2] = (byte)value; } }); } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// /// Method that removes the pixels that are flagged as background /// /// The bitmap you want to remove the background from /// The Bitmap without the background public static Bitmap RemoveBG(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 { 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); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; 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); return inputBitmap; } /// /// Method that removes all the useless things from the image and returns hopefully only the numbers /// /// The bitmap you want to remove useless things from (Expects a cropped part of the TyreWindow) /// The bitmap with (hopefully) only the digits public unsafe static Bitmap RemoveUseless(Bitmap inputBitmap) { //Note you can use something else than a cropped tyre window but I would recommend checking the code first to see if it fits your intended use 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; byte* ptr = (byte*)bmpData.Scan0.ToPointer(); for (int y = 0; y < inputBitmap.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); List pixelsToRemove = new List(); bool fromBorder = true; for (int x = 0; x < inputBitmap.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (fromBorder && B < F1TV_BACKGROUND_TRESHOLD.B && G < F1TV_BACKGROUND_TRESHOLD.G && R < F1TV_BACKGROUND_TRESHOLD.R) { pixelsToRemove.Add(x); } else { if (fromBorder) { fromBorder = false; pixelsToRemove.Add(x); } } } fromBorder = true; for (int x = inputBitmap.Width - 1; x > 0; x--) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (fromBorder && B < F1TV_BACKGROUND_TRESHOLD.B && G < F1TV_BACKGROUND_TRESHOLD.G && R < F1TV_BACKGROUND_TRESHOLD.R) { pixelsToRemove.Add(x); } else { if (fromBorder) { fromBorder = false; pixelsToRemove.Add(x); } } } foreach (int pxPos in pixelsToRemove) { byte* pixel = currentLine + (pxPos * bytesPerPixel); pixel[0] = 0xFF; pixel[1] = 0xFF; pixel[2] = 0xFF; } } //Removing the color parts 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); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (R >= F1TV_BACKGROUND_TRESHOLD.R + 15 || G >= F1TV_BACKGROUND_TRESHOLD.G + 15 || B >= F1TV_BACKGROUND_TRESHOLD.B + 15) { pixel[0] = 0xFF; pixel[1] = 0xFF; pixel[2] = 0xFF; } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// /// Recovers the average colors from the Image. NOTE : It wont take in account colors that are lower than the background /// /// The bitmap you want to get the average color from /// The average color of the bitmap public static Color GetAvgColorFromBitmap(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; int totR = 0; int totG = 0; int totB = 0; int totPixels = 1; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); int bmpHeight = inputBitmap.Height; int bmpWidth = inputBitmap.Width; Parallel.For(0, bmpHeight, y => { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < bmpWidth; x++) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (R >= F1TV_BACKGROUND_TRESHOLD.R || G >= F1TV_BACKGROUND_TRESHOLD.G || B >= F1TV_BACKGROUND_TRESHOLD.B) { totPixels++; totB += pixel[0]; totG += pixel[1]; totR += pixel[2]; } } }); } inputBitmap.UnlockBits(bmpData); return Color.FromArgb(255, Convert.ToInt32((float)totR / (float)totPixels), Convert.ToInt32((float)totG / (float)totPixels), Convert.ToInt32((float)totB / (float)totPixels)); } /// /// This method simply inverts all the colors in a Bitmap /// /// the bitmap you want to invert the colors from /// The bitmap with inverted colors public static Bitmap InvertColors(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 { 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); pixel[0] = (byte)(255 - pixel[0]); pixel[1] = (byte)(255 - pixel[1]); pixel[2] = (byte)(255 - pixel[2]); } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// /// Methods that applies Bicubic interpolation to increase the size and resolution of an image /// /// The bitmap you want to resize /// The factor of resizing you want to use. I recommend using even numbers /// The bitmap witht the new size public static Bitmap Resize(Bitmap inputBitmap, int resizeFactor) { var resultBitmap = new Bitmap(inputBitmap.Width * resizeFactor, inputBitmap.Height * resizeFactor); using (var graphics = Graphics.FromImage(resultBitmap)) { graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.DrawImage(inputBitmap, new Rectangle(0, 0, resultBitmap.Width, resultBitmap.Height)); } return resultBitmap; } /// /// method that Highlights the countours of a Bitmap /// /// The bitmap you want to highlight the countours of /// The bitmap with countours highlighted public static Bitmap HighlightContours(Bitmap inputBitmap) { Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); Bitmap grayscale = Grayscale(inputBitmap); Bitmap thresholded = Tresholding(grayscale, 128); Bitmap dilated = Dilatation(thresholded, 3); Bitmap eroded = Erode(dilated, 3); for (int y = 0; y < inputBitmap.Height; y++) { for (int x = 0; x < inputBitmap.Width; x++) { Color pixel = inputBitmap.GetPixel(x, y); Color dilatedPixel = dilated.GetPixel(x, y); Color erodedPixel = eroded.GetPixel(x, y); int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); int threshold = dilatedPixel.R; if (gray > threshold) { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); } else if (gray <= threshold && erodedPixel.R == 0) { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 0, 0)); } else { outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); } } } return outputBitmap; } /// /// Method that that erodes the morphology of a bitmap /// /// The bitmap you want to erode /// The amount of Erosion you want (be carefull its expensive on ressources) /// The Bitmap with the eroded contents public static Bitmap Erode(Bitmap inputBitmap, int kernelSize) { Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); int[,] kernel = new int[kernelSize, kernelSize]; for (int i = 0; i < kernelSize; i++) { for (int j = 0; j < kernelSize; j++) { kernel[i, j] = 1; } } for (int y = kernelSize / 2; y < inputBitmap.Height - kernelSize / 2; y++) { for (int x = kernelSize / 2; x < inputBitmap.Width - kernelSize / 2; x++) { bool flag = true; for (int i = -kernelSize / 2; i <= kernelSize / 2; i++) { for (int j = -kernelSize / 2; j <= kernelSize / 2; j++) { Color pixel = inputBitmap.GetPixel(x + i, y + j); int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); if (gray >= 128 && kernel[i + kernelSize / 2, j + kernelSize / 2] == 1) { flag = false; break; } } if (!flag) { break; } } if (flag) { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); } else { outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); } } } return outputBitmap; } /// /// Method that that use dilatation of the morphology of a bitmap /// /// The bitmap you want to use dilatation on /// The amount of dilatation you want (be carefull its expensive on ressources) /// The Bitmap after Dilatation public static Bitmap Dilatation(Bitmap inputBitmap, int kernelSize) { Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); int[,] kernel = new int[kernelSize, kernelSize]; for (int i = 0; i < kernelSize; i++) { for (int j = 0; j < kernelSize; j++) { kernel[i, j] = 1; } } for (int y = kernelSize / 2; y < inputBitmap.Height - kernelSize / 2; y++) { for (int x = kernelSize / 2; x < inputBitmap.Width - kernelSize / 2; x++) { bool flag = false; for (int i = -kernelSize / 2; i <= kernelSize / 2; i++) { for (int j = -kernelSize / 2; j <= kernelSize / 2; j++) { Color pixel = inputBitmap.GetPixel(x + i, y + j); int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); if (gray < 128 && kernel[i + kernelSize / 2, j + kernelSize / 2] == 1) { flag = true; break; } } if (flag) { break; } } if (flag) { outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); } else { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); } } } return outputBitmap; } } }","title":"OcrImage.cs"},{"location":"Code/OcrImage.html#ocrimagecs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : OcrImage.cs /// Brief : Class containing all the methods used to enhance images for OCR /// Version : 0.1 using System; using System.Collections.Generic; using System.Threading.Tasks; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; namespace Test_Merge { public class OcrImage { //this is a hardcoded value based on the colors of the F1TV data channel background you can change it if sometime in the future the color changes //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; public enum WindowType { LapTime, Text, Sector, Gap, Tyre, } /// /// Create a new Ocr image to help enhance the given bitmap for OCR /// /// The image you want to enhance public OcrImage(Bitmap inputBitmap) { InputBitmap = inputBitmap; } /// /// Enhances the image depending on wich type of window the image comes from /// /// The type of the window. Depending on it different enhancing features will be applied /// The enhanced Bitmap public Bitmap Enhance(WindowType type = WindowType.Text) { Bitmap outputBitmap = (Bitmap)InputBitmap.Clone(); switch (type) { case WindowType.LapTime: outputBitmap = Tresholding(outputBitmap, 185); outputBitmap = Resize(outputBitmap, 2); outputBitmap = Dilatation(outputBitmap, 1); outputBitmap = Erode(outputBitmap, 1); break; case WindowType.Text: outputBitmap = InvertColors(outputBitmap); outputBitmap = Tresholding(outputBitmap, 165); outputBitmap = Resize(outputBitmap, 2); outputBitmap = Dilatation(outputBitmap, 1); break; case WindowType.Tyre: outputBitmap = RemoveUseless(outputBitmap); outputBitmap = Resize(outputBitmap, 4); outputBitmap = Dilatation(outputBitmap, 1); break; default: outputBitmap = Tresholding(outputBitmap, 165); outputBitmap = Resize(outputBitmap, 4); outputBitmap = Erode(outputBitmap, 1); break; } return outputBitmap; } /// /// Method that convert a colored RGB bitmap into a GrayScale image /// /// The Bitmap you want to convert /// The bitmap in grayscale 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); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8; unsafe { 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); byte blue = pixel[0]; byte green = pixel[1]; byte red = pixel[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); pixel[0] = pixel[1] = pixel[2] = (byte)gray; } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// /// Method that binaries the input image up to a certain treshold given /// /// the bitmap you want to convert to binary colors /// The floor at wich the color is considered as white or black /// The binarised bitmap public static Bitmap Tresholding(Bitmap inputBitmap, int threshold) { 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 { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); int bmpHeight = inputBitmap.Height; int bmpWidth = inputBitmap.Width; Parallel.For(0, bmpHeight, y => { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < bmpWidth; x++) { byte* pixel = currentLine + (x * bytesPerPixel); byte blue = pixel[0]; byte green = pixel[1]; byte red = pixel[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); int value = gray < threshold ? 0 : 255; pixel[0] = pixel[1] = pixel[2] = (byte)value; } }); } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// /// Method that removes the pixels that are flagged as background /// /// The bitmap you want to remove the background from /// The Bitmap without the background public static Bitmap RemoveBG(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 { 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); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; 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); return inputBitmap; } /// /// Method that removes all the useless things from the image and returns hopefully only the numbers /// /// The bitmap you want to remove useless things from (Expects a cropped part of the TyreWindow) /// The bitmap with (hopefully) only the digits public unsafe static Bitmap RemoveUseless(Bitmap inputBitmap) { //Note you can use something else than a cropped tyre window but I would recommend checking the code first to see if it fits your intended use 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; byte* ptr = (byte*)bmpData.Scan0.ToPointer(); for (int y = 0; y < inputBitmap.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); List pixelsToRemove = new List(); bool fromBorder = true; for (int x = 0; x < inputBitmap.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (fromBorder && B < F1TV_BACKGROUND_TRESHOLD.B && G < F1TV_BACKGROUND_TRESHOLD.G && R < F1TV_BACKGROUND_TRESHOLD.R) { pixelsToRemove.Add(x); } else { if (fromBorder) { fromBorder = false; pixelsToRemove.Add(x); } } } fromBorder = true; for (int x = inputBitmap.Width - 1; x > 0; x--) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (fromBorder && B < F1TV_BACKGROUND_TRESHOLD.B && G < F1TV_BACKGROUND_TRESHOLD.G && R < F1TV_BACKGROUND_TRESHOLD.R) { pixelsToRemove.Add(x); } else { if (fromBorder) { fromBorder = false; pixelsToRemove.Add(x); } } } foreach (int pxPos in pixelsToRemove) { byte* pixel = currentLine + (pxPos * bytesPerPixel); pixel[0] = 0xFF; pixel[1] = 0xFF; pixel[2] = 0xFF; } } //Removing the color parts 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); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (R >= F1TV_BACKGROUND_TRESHOLD.R + 15 || G >= F1TV_BACKGROUND_TRESHOLD.G + 15 || B >= F1TV_BACKGROUND_TRESHOLD.B + 15) { pixel[0] = 0xFF; pixel[1] = 0xFF; pixel[2] = 0xFF; } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// /// Recovers the average colors from the Image. NOTE : It wont take in account colors that are lower than the background /// /// The bitmap you want to get the average color from /// The average color of the bitmap public static Color GetAvgColorFromBitmap(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; int totR = 0; int totG = 0; int totB = 0; int totPixels = 1; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); int bmpHeight = inputBitmap.Height; int bmpWidth = inputBitmap.Width; Parallel.For(0, bmpHeight, y => { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < bmpWidth; x++) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (R >= F1TV_BACKGROUND_TRESHOLD.R || G >= F1TV_BACKGROUND_TRESHOLD.G || B >= F1TV_BACKGROUND_TRESHOLD.B) { totPixels++; totB += pixel[0]; totG += pixel[1]; totR += pixel[2]; } } }); } inputBitmap.UnlockBits(bmpData); return Color.FromArgb(255, Convert.ToInt32((float)totR / (float)totPixels), Convert.ToInt32((float)totG / (float)totPixels), Convert.ToInt32((float)totB / (float)totPixels)); } /// /// This method simply inverts all the colors in a Bitmap /// /// the bitmap you want to invert the colors from /// The bitmap with inverted colors public static Bitmap InvertColors(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 { 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); pixel[0] = (byte)(255 - pixel[0]); pixel[1] = (byte)(255 - pixel[1]); pixel[2] = (byte)(255 - pixel[2]); } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// /// Methods that applies Bicubic interpolation to increase the size and resolution of an image /// /// The bitmap you want to resize /// The factor of resizing you want to use. I recommend using even numbers /// The bitmap witht the new size public static Bitmap Resize(Bitmap inputBitmap, int resizeFactor) { var resultBitmap = new Bitmap(inputBitmap.Width * resizeFactor, inputBitmap.Height * resizeFactor); using (var graphics = Graphics.FromImage(resultBitmap)) { graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.DrawImage(inputBitmap, new Rectangle(0, 0, resultBitmap.Width, resultBitmap.Height)); } return resultBitmap; } /// /// method that Highlights the countours of a Bitmap /// /// The bitmap you want to highlight the countours of /// The bitmap with countours highlighted public static Bitmap HighlightContours(Bitmap inputBitmap) { Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); Bitmap grayscale = Grayscale(inputBitmap); Bitmap thresholded = Tresholding(grayscale, 128); Bitmap dilated = Dilatation(thresholded, 3); Bitmap eroded = Erode(dilated, 3); for (int y = 0; y < inputBitmap.Height; y++) { for (int x = 0; x < inputBitmap.Width; x++) { Color pixel = inputBitmap.GetPixel(x, y); Color dilatedPixel = dilated.GetPixel(x, y); Color erodedPixel = eroded.GetPixel(x, y); int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); int threshold = dilatedPixel.R; if (gray > threshold) { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); } else if (gray <= threshold && erodedPixel.R == 0) { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 0, 0)); } else { outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); } } } return outputBitmap; } /// /// Method that that erodes the morphology of a bitmap /// /// The bitmap you want to erode /// The amount of Erosion you want (be carefull its expensive on ressources) /// The Bitmap with the eroded contents public static Bitmap Erode(Bitmap inputBitmap, int kernelSize) { Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); int[,] kernel = new int[kernelSize, kernelSize]; for (int i = 0; i < kernelSize; i++) { for (int j = 0; j < kernelSize; j++) { kernel[i, j] = 1; } } for (int y = kernelSize / 2; y < inputBitmap.Height - kernelSize / 2; y++) { for (int x = kernelSize / 2; x < inputBitmap.Width - kernelSize / 2; x++) { bool flag = true; for (int i = -kernelSize / 2; i <= kernelSize / 2; i++) { for (int j = -kernelSize / 2; j <= kernelSize / 2; j++) { Color pixel = inputBitmap.GetPixel(x + i, y + j); int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); if (gray >= 128 && kernel[i + kernelSize / 2, j + kernelSize / 2] == 1) { flag = false; break; } } if (!flag) { break; } } if (flag) { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); } else { outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); } } } return outputBitmap; } /// /// Method that that use dilatation of the morphology of a bitmap /// /// The bitmap you want to use dilatation on /// The amount of dilatation you want (be carefull its expensive on ressources) /// The Bitmap after Dilatation public static Bitmap Dilatation(Bitmap inputBitmap, int kernelSize) { Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); int[,] kernel = new int[kernelSize, kernelSize]; for (int i = 0; i < kernelSize; i++) { for (int j = 0; j < kernelSize; j++) { kernel[i, j] = 1; } } for (int y = kernelSize / 2; y < inputBitmap.Height - kernelSize / 2; y++) { for (int x = kernelSize / 2; x < inputBitmap.Width - kernelSize / 2; x++) { bool flag = false; for (int i = -kernelSize / 2; i <= kernelSize / 2; i++) { for (int j = -kernelSize / 2; j <= kernelSize / 2; j++) { Color pixel = inputBitmap.GetPixel(x + i, y + j); int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); if (gray < 128 && kernel[i + kernelSize / 2, j + kernelSize / 2] == 1) { flag = true; break; } } if (flag) { break; } } if (flag) { outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); } else { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); } } } return outputBitmap; } } }","title":"OcrImage.cs"},{"location":"Code/Settings.html","text":"Settings.cs \ufeffusing System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.IO; namespace Test_Merge { public partial class Settings : Form { private string _grandPrixUrl = \"\"; private string _grandPrixName = \"\"; private int _grandPrixYear = 2000; private List _driverList = new List(); private F1TVEmulator Emulator = null; private ConfigurationTool Config = null; private bool CreatingZone = false; private Point ZoneP1; private Point ZoneP2; private bool CreatingWindow = false; private Point WindowP1; private Point WindowP2; List WindowsToAdd = new List(); public string GrandPrixUrl { get => _grandPrixUrl; private set => _grandPrixUrl = value; } public string GrandPrixName { get => _grandPrixName; private set => _grandPrixName = value; } public int GrandPrixYear { get => _grandPrixYear; private set => _grandPrixYear = value; } public List DriverList { get => _driverList; private set => _driverList = value; } public Settings() { InitializeComponent(); Load(); } private void Load() { RefreshUI(); } private void RefreshUI() { lsbDrivers.DataSource = null; lsbDrivers.DataSource = DriverList; if (Directory.Exists(ConfigurationTool.CONFIGS_FOLDER_NAME)) { lsbPresets.DataSource = null; lsbPresets.DataSource = Directory.GetFiles(ConfigurationTool.CONFIGS_FOLDER_NAME); } if (CreatingZone) { if (ZoneP1 == new Point(-1, -1)) { lblZonePointsRemaning.Text = \"2 points Remaining\"; } else { lblZonePointsRemaning.Text = \"1 point Remaining\"; } } else { lblZonePointsRemaning.Text = \"\"; } if (CreatingWindow) { if (WindowP1 == new Point(-1, -1)) { lblWindowPointsRemaining.Text = \"2 points Remaining\"; } else { lblWindowPointsRemaining.Text = \"1 point Remaining\"; } lblWindowPointsRemaining.Text = ConfigurationTool.NUMBER_OF_ZONES - WindowsToAdd.Count() + \" Windows remaining\"; } else { lblWindowPointsRemaining.Text = \"\"; lblWindowsRemaining.Text = \"\"; } if (Config != null) { pbxMain.Image = Config.MainZone.Draw(); if(Config.MainZone.Zones.Count > 0) pbxDriverZone.Image = Config.MainZone.Zones[0].Draw(); } } private void CreateNewZone(Point p1, Point p2) { Rectangle dimensions = CreateAbsoluteRectangle(p1, p2); Config = new ConfigurationTool((Bitmap)pbxMain.Image, dimensions); RefreshUI(); } private void CreateWindows(List dimensions) { if (Config != null) { Config.AddWindows(dimensions); } } private void tbxGpUrl_TextChanged(object sender, EventArgs e) { GrandPrixUrl = tbxGpUrl.Text; } private void tbxGpName_TextChanged(object sender, EventArgs e) { GrandPrixName = tbxGpName.Text; } private void tbxGpYear_TextChanged(object sender, EventArgs e) { int year; try { year = Convert.ToInt32(tbxGpYear.Text); } catch { year = 1545; } GrandPrixYear = year; } private void btnAddDriver_Click(object sender, EventArgs e) { string newDriver = tbxDriverName.Text; DriverList.Add(newDriver); tbxDriverName.Text = \"\"; RefreshUI(); } private void btnRemoveDriver_Click(object sender, EventArgs e) { if (lsbDrivers.SelectedIndex >= 0) { DriverList.RemoveAt(lsbDrivers.SelectedIndex); } RefreshUI(); } private void SwitchZoneCreation() { if (CreatingZone) { CreatingZone = false; lblZonePointsRemaning.Text = \"\"; } else { CreatingZone = true; if (Config != null) Config.ResetMainZone(); if (CreatingWindow) SwitchWindowCreation(); if (Emulator != null && Emulator.Ready) { Config = null; pbxMain.Image = Emulator.Screenshot(); } ZoneP1 = new Point(-1, -1); ZoneP2 = new Point(-1, -1); lblZonePointsRemaning.Text = \"2 Points left\"; } RefreshUI(); } private void SwitchWindowCreation() { if (CreatingWindow) { CreatingWindow = false; } else { CreatingWindow = true; if (Config != null) Config.ResetWindows(); if (CreatingZone) SwitchZoneCreation(); WindowP1 = new Point(-1, -1); WindowP2 = new Point(-1, -1); WindowsToAdd = new List(); } RefreshUI(); } private void btnCreatZone_Click(object sender, EventArgs e) { SwitchZoneCreation(); } private void btnCreateWindow_Click(object sender, EventArgs e) { SwitchWindowCreation(); } private void pbxMain_MouseClick(object sender, MouseEventArgs e) { if (CreatingZone && pbxMain.Image != null) { //Point coordinates = pbxMain.PointToClient(new Point(MousePosition.X, MousePosition.Y)); Point coordinates = e.Location; float xOffset = (float)pbxMain.Image.Width / (float)pbxMain.Width; float yOffset = (float)pbxMain.Image.Height / (float)pbxMain.Height; Point newPoint = new Point(Convert.ToInt32((float)coordinates.X * xOffset), Convert.ToInt32((float)coordinates.Y * yOffset)); //MessageBox.Show(\"Coordinates\" + Environment.NewLine + \"Old : \" + coordinates.ToString() + Environment.NewLine + \"New : \" + newPoint.ToString()); if (ZoneP1 == new Point(-1, -1)) { ZoneP1 = newPoint; } else { ZoneP2 = newPoint; CreateNewZone(ZoneP1, ZoneP2); SwitchZoneCreation(); } RefreshUI(); } } private void pbxMain_Click(object sender, EventArgs e) { //Not the right one to use visibly } private void pbxDriverZone_MouseClick(object sender, MouseEventArgs e) { if (CreatingWindow && pbxDriverZone.Image != null) { Point coordinates = e.Location; float xOffset = (float)pbxDriverZone.Image.Width / (float)pbxDriverZone.Width; float yOffset = (float)pbxDriverZone.Image.Height / (float)pbxDriverZone.Height; Point newPoint = new Point(Convert.ToInt32((float)coordinates.X * xOffset), Convert.ToInt32((float)coordinates.Y * yOffset)); if (WindowP1 == new Point(-1, -1)) { WindowP1 = newPoint; } else { WindowP2 = newPoint; WindowsToAdd.Add(CreateAbsoluteRectangle(WindowP1, WindowP2)); if (WindowsToAdd.Count < ConfigurationTool.NUMBER_OF_ZONES) { WindowP1 = new Point(-1, -1); WindowP2 = new Point(-1, -1); } else { WindowP1 = new Point(WindowP1.X, 0); WindowP2 = new Point(WindowP2.X, pbxDriverZone.Image.Height); CreateWindows(WindowsToAdd); SwitchWindowCreation(); } } RefreshUI(); } } private void pbxDriverZone_Click(object sender, EventArgs e) { //Not the right one to use visibly } private Rectangle CreateAbsoluteRectangle(Point p1, Point p2) { Point newP1 = new Point(); Point newP2 = new Point(); if (p1.X < p2.X) { newP1.X = p1.X; newP2.X = p2.X; } else { newP1.X = p2.X; newP2.X = p1.X; } if (p1.Y < p2.Y) { newP1.Y = p1.Y; newP2.Y = p2.Y; } else { newP1.Y = p2.Y; newP2.Y = p1.Y; } return new Rectangle(newP1.X, newP1.Y, newP2.X - newP1.X, newP2.Y - newP1.Y); } private async void btnRefresh_Click(object sender, EventArgs e) { btnRefresh.Enabled = false; if (Emulator == null || Emulator.GrandPrixUrl != tbxGpUrl.Text) { Emulator = new F1TVEmulator(tbxGpUrl.Text); } if (!Emulator.Ready) { Task start = Task.Run(() => Emulator.Start()); int errorCode = await start; if (errorCode != 0) { string message; switch (errorCode) { case 101: message = \"Error \" + errorCode + \" Could not start the driver. It could be because an other instance is runnin make sure you closed them all before trying again\"; break; case 102: message = \"Error \" + errorCode + \" Could not navigate on the F1TV site. Make sure the correct URL has been given and that you logged from chrome. It can take a few minutes to update\"; break; case 103: message = \"Error \" + errorCode + \" The url is not a valid url\"; break; case 104: message = \"Error \" + errorCode + \" The url is not a valid url\"; break; case 105: message = \"Error \" + errorCode + \" There has been an error trying to emulate button presses. Please try again\"; break; case 106: message = \"Error \" + errorCode + \" There has been an error trying to emulate button presses. Please try again\"; break; default: message = \"Could not start the emulator Error \" + errorCode; break; } MessageBox.Show(message); } else { pbxMain.Image = Emulator.Screenshot(); } } else { pbxMain.Image = Emulator.Screenshot(); } btnRefresh.Enabled = true; } private void Settings_FormClosing(object sender, FormClosingEventArgs e) { if (Emulator != null) { Emulator.Stop(); } } private void btnResetDriver_Click(object sender, EventArgs e) { if (Emulator != null) { Emulator.ResetDriver(); } } private void btnSavePreset_Click(object sender, EventArgs e) { string presetName = tbxPresetName.Text; if (Config != null) { Config.SaveToJson(DriverList,presetName); } RefreshUI(); } private void lsbPresets_SelectedIndexChanged(object sender, EventArgs e) { //Nothing } private void btnLoadPreset_Click(object sender, EventArgs e) { if (lsbPresets.SelectedIndex >= 0 && pbxMain.Image != null) { try { Reader reader = new Reader(lsbPresets.Items[lsbPresets.SelectedIndex].ToString(), (Bitmap)pbxMain.Image,false); //MainZones #0 is the big main zone containing driver zones Config = new ConfigurationTool((Bitmap)pbxMain.Image, reader.MainZones[0].Bounds); Config.MainZone = reader.MainZones[0]; DriverList = reader.Drivers; } catch (Exception ex) { MessageBox.Show(\"Could not load the settings error :\" + ex); } RefreshUI(); } } } }","title":"Settings.cs"},{"location":"Code/Settings.html#settingscs","text":"\ufeffusing System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.IO; namespace Test_Merge { public partial class Settings : Form { private string _grandPrixUrl = \"\"; private string _grandPrixName = \"\"; private int _grandPrixYear = 2000; private List _driverList = new List(); private F1TVEmulator Emulator = null; private ConfigurationTool Config = null; private bool CreatingZone = false; private Point ZoneP1; private Point ZoneP2; private bool CreatingWindow = false; private Point WindowP1; private Point WindowP2; List WindowsToAdd = new List(); public string GrandPrixUrl { get => _grandPrixUrl; private set => _grandPrixUrl = value; } public string GrandPrixName { get => _grandPrixName; private set => _grandPrixName = value; } public int GrandPrixYear { get => _grandPrixYear; private set => _grandPrixYear = value; } public List DriverList { get => _driverList; private set => _driverList = value; } public Settings() { InitializeComponent(); Load(); } private void Load() { RefreshUI(); } private void RefreshUI() { lsbDrivers.DataSource = null; lsbDrivers.DataSource = DriverList; if (Directory.Exists(ConfigurationTool.CONFIGS_FOLDER_NAME)) { lsbPresets.DataSource = null; lsbPresets.DataSource = Directory.GetFiles(ConfigurationTool.CONFIGS_FOLDER_NAME); } if (CreatingZone) { if (ZoneP1 == new Point(-1, -1)) { lblZonePointsRemaning.Text = \"2 points Remaining\"; } else { lblZonePointsRemaning.Text = \"1 point Remaining\"; } } else { lblZonePointsRemaning.Text = \"\"; } if (CreatingWindow) { if (WindowP1 == new Point(-1, -1)) { lblWindowPointsRemaining.Text = \"2 points Remaining\"; } else { lblWindowPointsRemaining.Text = \"1 point Remaining\"; } lblWindowPointsRemaining.Text = ConfigurationTool.NUMBER_OF_ZONES - WindowsToAdd.Count() + \" Windows remaining\"; } else { lblWindowPointsRemaining.Text = \"\"; lblWindowsRemaining.Text = \"\"; } if (Config != null) { pbxMain.Image = Config.MainZone.Draw(); if(Config.MainZone.Zones.Count > 0) pbxDriverZone.Image = Config.MainZone.Zones[0].Draw(); } } private void CreateNewZone(Point p1, Point p2) { Rectangle dimensions = CreateAbsoluteRectangle(p1, p2); Config = new ConfigurationTool((Bitmap)pbxMain.Image, dimensions); RefreshUI(); } private void CreateWindows(List dimensions) { if (Config != null) { Config.AddWindows(dimensions); } } private void tbxGpUrl_TextChanged(object sender, EventArgs e) { GrandPrixUrl = tbxGpUrl.Text; } private void tbxGpName_TextChanged(object sender, EventArgs e) { GrandPrixName = tbxGpName.Text; } private void tbxGpYear_TextChanged(object sender, EventArgs e) { int year; try { year = Convert.ToInt32(tbxGpYear.Text); } catch { year = 1545; } GrandPrixYear = year; } private void btnAddDriver_Click(object sender, EventArgs e) { string newDriver = tbxDriverName.Text; DriverList.Add(newDriver); tbxDriverName.Text = \"\"; RefreshUI(); } private void btnRemoveDriver_Click(object sender, EventArgs e) { if (lsbDrivers.SelectedIndex >= 0) { DriverList.RemoveAt(lsbDrivers.SelectedIndex); } RefreshUI(); } private void SwitchZoneCreation() { if (CreatingZone) { CreatingZone = false; lblZonePointsRemaning.Text = \"\"; } else { CreatingZone = true; if (Config != null) Config.ResetMainZone(); if (CreatingWindow) SwitchWindowCreation(); if (Emulator != null && Emulator.Ready) { Config = null; pbxMain.Image = Emulator.Screenshot(); } ZoneP1 = new Point(-1, -1); ZoneP2 = new Point(-1, -1); lblZonePointsRemaning.Text = \"2 Points left\"; } RefreshUI(); } private void SwitchWindowCreation() { if (CreatingWindow) { CreatingWindow = false; } else { CreatingWindow = true; if (Config != null) Config.ResetWindows(); if (CreatingZone) SwitchZoneCreation(); WindowP1 = new Point(-1, -1); WindowP2 = new Point(-1, -1); WindowsToAdd = new List(); } RefreshUI(); } private void btnCreatZone_Click(object sender, EventArgs e) { SwitchZoneCreation(); } private void btnCreateWindow_Click(object sender, EventArgs e) { SwitchWindowCreation(); } private void pbxMain_MouseClick(object sender, MouseEventArgs e) { if (CreatingZone && pbxMain.Image != null) { //Point coordinates = pbxMain.PointToClient(new Point(MousePosition.X, MousePosition.Y)); Point coordinates = e.Location; float xOffset = (float)pbxMain.Image.Width / (float)pbxMain.Width; float yOffset = (float)pbxMain.Image.Height / (float)pbxMain.Height; Point newPoint = new Point(Convert.ToInt32((float)coordinates.X * xOffset), Convert.ToInt32((float)coordinates.Y * yOffset)); //MessageBox.Show(\"Coordinates\" + Environment.NewLine + \"Old : \" + coordinates.ToString() + Environment.NewLine + \"New : \" + newPoint.ToString()); if (ZoneP1 == new Point(-1, -1)) { ZoneP1 = newPoint; } else { ZoneP2 = newPoint; CreateNewZone(ZoneP1, ZoneP2); SwitchZoneCreation(); } RefreshUI(); } } private void pbxMain_Click(object sender, EventArgs e) { //Not the right one to use visibly } private void pbxDriverZone_MouseClick(object sender, MouseEventArgs e) { if (CreatingWindow && pbxDriverZone.Image != null) { Point coordinates = e.Location; float xOffset = (float)pbxDriverZone.Image.Width / (float)pbxDriverZone.Width; float yOffset = (float)pbxDriverZone.Image.Height / (float)pbxDriverZone.Height; Point newPoint = new Point(Convert.ToInt32((float)coordinates.X * xOffset), Convert.ToInt32((float)coordinates.Y * yOffset)); if (WindowP1 == new Point(-1, -1)) { WindowP1 = newPoint; } else { WindowP2 = newPoint; WindowsToAdd.Add(CreateAbsoluteRectangle(WindowP1, WindowP2)); if (WindowsToAdd.Count < ConfigurationTool.NUMBER_OF_ZONES) { WindowP1 = new Point(-1, -1); WindowP2 = new Point(-1, -1); } else { WindowP1 = new Point(WindowP1.X, 0); WindowP2 = new Point(WindowP2.X, pbxDriverZone.Image.Height); CreateWindows(WindowsToAdd); SwitchWindowCreation(); } } RefreshUI(); } } private void pbxDriverZone_Click(object sender, EventArgs e) { //Not the right one to use visibly } private Rectangle CreateAbsoluteRectangle(Point p1, Point p2) { Point newP1 = new Point(); Point newP2 = new Point(); if (p1.X < p2.X) { newP1.X = p1.X; newP2.X = p2.X; } else { newP1.X = p2.X; newP2.X = p1.X; } if (p1.Y < p2.Y) { newP1.Y = p1.Y; newP2.Y = p2.Y; } else { newP1.Y = p2.Y; newP2.Y = p1.Y; } return new Rectangle(newP1.X, newP1.Y, newP2.X - newP1.X, newP2.Y - newP1.Y); } private async void btnRefresh_Click(object sender, EventArgs e) { btnRefresh.Enabled = false; if (Emulator == null || Emulator.GrandPrixUrl != tbxGpUrl.Text) { Emulator = new F1TVEmulator(tbxGpUrl.Text); } if (!Emulator.Ready) { Task start = Task.Run(() => Emulator.Start()); int errorCode = await start; if (errorCode != 0) { string message; switch (errorCode) { case 101: message = \"Error \" + errorCode + \" Could not start the driver. It could be because an other instance is runnin make sure you closed them all before trying again\"; break; case 102: message = \"Error \" + errorCode + \" Could not navigate on the F1TV site. Make sure the correct URL has been given and that you logged from chrome. It can take a few minutes to update\"; break; case 103: message = \"Error \" + errorCode + \" The url is not a valid url\"; break; case 104: message = \"Error \" + errorCode + \" The url is not a valid url\"; break; case 105: message = \"Error \" + errorCode + \" There has been an error trying to emulate button presses. Please try again\"; break; case 106: message = \"Error \" + errorCode + \" There has been an error trying to emulate button presses. Please try again\"; break; default: message = \"Could not start the emulator Error \" + errorCode; break; } MessageBox.Show(message); } else { pbxMain.Image = Emulator.Screenshot(); } } else { pbxMain.Image = Emulator.Screenshot(); } btnRefresh.Enabled = true; } private void Settings_FormClosing(object sender, FormClosingEventArgs e) { if (Emulator != null) { Emulator.Stop(); } } private void btnResetDriver_Click(object sender, EventArgs e) { if (Emulator != null) { Emulator.ResetDriver(); } } private void btnSavePreset_Click(object sender, EventArgs e) { string presetName = tbxPresetName.Text; if (Config != null) { Config.SaveToJson(DriverList,presetName); } RefreshUI(); } private void lsbPresets_SelectedIndexChanged(object sender, EventArgs e) { //Nothing } private void btnLoadPreset_Click(object sender, EventArgs e) { if (lsbPresets.SelectedIndex >= 0 && pbxMain.Image != null) { try { Reader reader = new Reader(lsbPresets.Items[lsbPresets.SelectedIndex].ToString(), (Bitmap)pbxMain.Image,false); //MainZones #0 is the big main zone containing driver zones Config = new ConfigurationTool((Bitmap)pbxMain.Image, reader.MainZones[0].Bounds); Config.MainZone = reader.MainZones[0]; DriverList = reader.Drivers; } catch (Exception ex) { MessageBox.Show(\"Could not load the settings error :\" + ex); } RefreshUI(); } } } }","title":"Settings.cs"},{"location":"Code/recoverCookiesCSV.html","text":"recoverCookiesCSV.py # Rohmer Maxime # RecoverCookies.py # Little script that recovers the cookies stored in the chrome sqlite database and then decrypts them using the key stored in the chrome files # This script has been created to be used by an other programm or for the data to not be used directly. This is why it stores all the decoded cookies in a csv. (Btw could be smart for the end programm to delete the csv after using it) # Parts of this cript have been created with the help of ChatGPT import os import json import base64 import sqlite3 import win32crypt from Cryptodome.Cipher import AES from pathlib import Path import csv def get_master_key(): with open( os.getenv(\"localappdata\") + \"\\\\Google\\\\Chrome\\\\User Data\\\\Local State\", \"r\" ) as f: local_state = f.read() local_state = json.loads(local_state) master_key = base64.b64decode(local_state[\"os_crypt\"][\"encrypted_key\"]) master_key = master_key[5:] # removing DPAPI master_key = win32crypt.CryptUnprotectData(master_key, None, None, None, 0)[1] print(\"MASTER KEY :\") print(master_key) print(len(master_key)) return master_key def decrypt_payload(cipher, payload): return cipher.decrypt(payload) def generate_cipher(aes_key, iv): return AES.new(aes_key, AES.MODE_GCM, iv) def decrypt_password(buff, master_key): try: iv = buff[3:15] payload = buff[15:] cipher = generate_cipher(master_key, iv) decrypted_pass = decrypt_payload(cipher, payload) decrypted_pass = decrypted_pass[:-16].decode() # remove suffix bytes return decrypted_pass except Exception: # print(\"Probably saved password from Chrome version older than v80\\n\") # print(str(e)) return \"Chrome < 80\" master_key = get_master_key() cookies_path = Path( os.getenv(\"localappdata\") + \"\\\\Google\\\\Chrome\\\\User Data\\\\Default\\\\Network\\\\Cookies\" ) if not cookies_path.exists(): raise ValueError(\"Cookies file not found\") with sqlite3.connect(cookies_path) as connection: connection.row_factory = sqlite3.Row cursor = connection.cursor() cursor.execute(\"SELECT * FROM cookies\") with open('cookies.csv', 'a', newline='') as csvfile: fieldnames = ['host_key', 'name', 'value', 'path', 'expires_utc', 'is_secure', 'is_httponly'] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) if csvfile.tell() == 0: writer.writeheader() for row in cursor.fetchall(): decrypted_value = decrypt_password(row[\"encrypted_value\"], master_key) writer.writerow({ 'host_key': row[\"host_key\"], 'name': row[\"name\"], 'value': decrypted_value, 'path': row[\"path\"], 'expires_utc': row[\"expires_utc\"], 'is_secure': row[\"is_secure\"], 'is_httponly': row[\"is_httponly\"] }) print(\"Finished CSV\")","title":"recoverCookiesCSV.py"},{"location":"Code/recoverCookiesCSV.html#recovercookiescsvpy","text":"# Rohmer Maxime # RecoverCookies.py # Little script that recovers the cookies stored in the chrome sqlite database and then decrypts them using the key stored in the chrome files # This script has been created to be used by an other programm or for the data to not be used directly. This is why it stores all the decoded cookies in a csv. (Btw could be smart for the end programm to delete the csv after using it) # Parts of this cript have been created with the help of ChatGPT import os import json import base64 import sqlite3 import win32crypt from Cryptodome.Cipher import AES from pathlib import Path import csv def get_master_key(): with open( os.getenv(\"localappdata\") + \"\\\\Google\\\\Chrome\\\\User Data\\\\Local State\", \"r\" ) as f: local_state = f.read() local_state = json.loads(local_state) master_key = base64.b64decode(local_state[\"os_crypt\"][\"encrypted_key\"]) master_key = master_key[5:] # removing DPAPI master_key = win32crypt.CryptUnprotectData(master_key, None, None, None, 0)[1] print(\"MASTER KEY :\") print(master_key) print(len(master_key)) return master_key def decrypt_payload(cipher, payload): return cipher.decrypt(payload) def generate_cipher(aes_key, iv): return AES.new(aes_key, AES.MODE_GCM, iv) def decrypt_password(buff, master_key): try: iv = buff[3:15] payload = buff[15:] cipher = generate_cipher(master_key, iv) decrypted_pass = decrypt_payload(cipher, payload) decrypted_pass = decrypted_pass[:-16].decode() # remove suffix bytes return decrypted_pass except Exception: # print(\"Probably saved password from Chrome version older than v80\\n\") # print(str(e)) return \"Chrome < 80\" master_key = get_master_key() cookies_path = Path( os.getenv(\"localappdata\") + \"\\\\Google\\\\Chrome\\\\User Data\\\\Default\\\\Network\\\\Cookies\" ) if not cookies_path.exists(): raise ValueError(\"Cookies file not found\") with sqlite3.connect(cookies_path) as connection: connection.row_factory = sqlite3.Row cursor = connection.cursor() cursor.execute(\"SELECT * FROM cookies\") with open('cookies.csv', 'a', newline='') as csvfile: fieldnames = ['host_key', 'name', 'value', 'path', 'expires_utc', 'is_secure', 'is_httponly'] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) if csvfile.tell() == 0: writer.writeheader() for row in cursor.fetchall(): decrypted_value = decrypt_password(row[\"encrypted_value\"], master_key) writer.writerow({ 'host_key': row[\"host_key\"], 'name': row[\"name\"], 'value': decrypted_value, 'path': row[\"path\"], 'expires_utc': row[\"expires_utc\"], 'is_secure': row[\"is_secure\"], 'is_httponly': row[\"is_httponly\"] }) print(\"Finished CSV\")","title":"recoverCookiesCSV.py"}]} \ No newline at end of file diff --git a/site/sitemap.xml.gz b/site/sitemap.xml.gz index e560093..8f99609 100644 Binary files a/site/sitemap.xml.gz and b/site/sitemap.xml.gz differ diff --git a/temp_annexes/Code/ConfigurationTool.md b/temp_annexes/Code/ConfigurationTool.md new file mode 100644 index 0000000..fc2b89a --- /dev/null +++ b/temp_annexes/Code/ConfigurationTool.md @@ -0,0 +1,198 @@ +# ConfigurationTool.cs + +``` cs +/// Author : Maxime Rohmer +/// Date : 08/05/2023 +/// File : ConfigurationTool.cs +/// Brief : Class that contains all the methods needed to create a config file for the OCR +/// Version : 0.1 + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Tesseract; +using System.IO; + +namespace Test_Merge +{ + public class ConfigurationTool + { + public Zone MainZone; + public const int NUMBER_OF_DRIVERS = 20; + public const int NUMBER_OF_ZONES = 9; + public const string CONFIGS_FOLDER_NAME = "./Presets/"; + + public ConfigurationTool(Bitmap fullImage, Rectangle mainZoneDimensions) + { + MainZone = new Zone(fullImage, mainZoneDimensions,"Main"); + AutoCalibrate(); + } + public void ResetMainZone() + { + MainZone.ResetZones(); + } + public void ResetWindows() + { + MainZone.ResetWindows(); + } + public void SaveToJson(List drivers, string configName) + { + string JSON = ""; + + JSON += "{" + Environment.NewLine; + JSON += MainZone.ToJSON() + "," + Environment.NewLine; + JSON += "\"Drivers\":[" + Environment.NewLine; + + for (int i = 0; i < drivers.Count; i++) + { + JSON += "\"" + drivers[i] + "\""; + if (i < drivers.Count - 1) + JSON += ","; + JSON += Environment.NewLine; + } + + JSON += "]" + Environment.NewLine; + + JSON += "}"; + + if (!Directory.Exists(CONFIGS_FOLDER_NAME)) + Directory.CreateDirectory(CONFIGS_FOLDER_NAME); + + string path = CONFIGS_FOLDER_NAME + configName; + + if (File.Exists(path + ".json")) + { + //We need to create a new name + int count = 2; + while (File.Exists(path + "_" + count + ".json")) + { + count++; + } + path += "_" + count + ".json"; + } + else + { + path += ".json"; + } + + File.WriteAllText(path, JSON); + } + public void AddWindows(List rectangles) + { + foreach (Zone driverZone in MainZone.Zones) + { + Bitmap zoneImage = driverZone.ZoneImage; + + for (int i = 1; i <= rectangles.Count; i++) + { + switch (i) + { + case 1: + //First zone should be the driver's Position + driverZone.AddWindow(new DriverPositionWindow(driverZone.ZoneImage, rectangles[i - 1], false)); + break; + case 2: + //First zone should be the Gap to leader + driverZone.AddWindow(new DriverGapToLeaderWindow(driverZone.ZoneImage, rectangles[i - 1], false)); + break; + case 3: + //First zone should be the driver's Lap Time + driverZone.AddWindow(new DriverLapTimeWindow(driverZone.ZoneImage, rectangles[i - 1], false)); + break; + case 4: + //First zone should be the driver's DRS status + driverZone.AddWindow(new DriverDrsWindow(driverZone.ZoneImage, rectangles[i - 1], false)); + break; + case 5: + //First zone should be the driver's Tyre's informations + driverZone.AddWindow(new DriverTyresWindow(driverZone.ZoneImage, rectangles[i - 1], false)); + break; + case 6: + //First zone should be the driver's Name + driverZone.AddWindow(new DriverNameWindow(driverZone.ZoneImage, rectangles[i - 1], false)); + break; + case 7: + //First zone should be the driver's First Sector + driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 1, false)); + break; + case 8: + //First zone should be the driver's Second Sector + driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 2, false)); + break; + case 9: + //First zone should be the driver's Position Sector + driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 3, false)); + break; + } + } + } + } + public void AutoCalibrate() + { + List detectedText = new List(); + List zones = new List(); + + TesseractEngine engine = new TesseractEngine(Window.TESS_DATA_FOLDER.FullName, "eng", EngineMode.Default); + Image image = MainZone.ZoneImage; + var tessImage = Pix.LoadFromMemory(Window.ImageToByte(image)); + + Page page = engine.Process(tessImage); + using (var iter = page.GetIterator()) + { + iter.Begin(); + do + { + Rect boundingBox; + if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox)) + { + //var text = iter.GetText(PageIteratorLevel.Word).ToUpper(); + //We remove all the rectangles that are definitely too big + if (boundingBox.Height < image.Height / NUMBER_OF_DRIVERS) + { + //Now we add a filter to only get the boxes in the right because they are much more reliable in size + if (boundingBox.X1 > image.Width / 2) + { + //Now we check if an other square box has been found roughly in the same y axis + bool match = false; + //The tolerance is roughly half the size that a window will be + int tolerance = (image.Height / NUMBER_OF_DRIVERS) / 2; + + foreach (Rectangle rect in detectedText) + { + if (rect.Y > boundingBox.Y1 - tolerance && rect.Y < boundingBox.Y1 + tolerance) + { + //There already is a rectangle in this line + match = true; + } + } + //if nothing matched we can add it + if (!match) + detectedText.Add(new Rectangle(boundingBox.X1, boundingBox.Y1, boundingBox.Width, boundingBox.Height)); + } + } + } + } while (iter.Next(PageIteratorLevel.Word)); + } + //DEBUG + int i = 1; + foreach (Rectangle Rectangle in detectedText) + { + Rectangle windowRectangle; + Size windowSize = new Size(image.Width, image.Height / NUMBER_OF_DRIVERS); + Point windowLocation = new Point(0, (Rectangle.Y + Rectangle.Height / 2) - windowSize.Height / 2); + windowRectangle = new Rectangle(windowLocation, windowSize); + //We add the driver zones + Zone driverZone = new Zone(MainZone.ZoneImage, windowRectangle, "DriverZone"); + MainZone.AddZone(driverZone); + + driverZone.ZoneImage.Save("Driver" + i+".png"); + i++; + } + } + } +} + +``` diff --git a/temp_annexes/Code/DriverData.md b/temp_annexes/Code/DriverData.md new file mode 100644 index 0000000..c75f38e --- /dev/null +++ b/temp_annexes/Code/DriverData.md @@ -0,0 +1,107 @@ +# DriverData.cs + +``` cs +/// Author : Maxime Rohmer +/// Date : 08/05/2023 +/// File : DriverData.cs +/// Brief : Class used to store Driver informations +/// Version : 0.1 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Test_Merge +{ + public class DriverData + { + public bool DRS; //True = Drs is opened + public int GapToLeader; //In ms + public int LapTime; //In ms + public string Name; //Ex: LECLERC + public int Position; //Ex: 1 + public int Sector1; //in ms + public int Sector2; //in ms + public int Sector3; //in ms + public Tyre CurrentTyre;//Ex Soft 11 laps + + public DriverData(bool dRS, int gapToLeader, int lapTime, string name, int position, int sector1, int sector2, int sector3, Tyre tyre) + { + DRS = dRS; + GapToLeader = gapToLeader; + LapTime = lapTime; + Name = name; + Position = position; + Sector1 = sector1; + Sector2 = sector2; + Sector3 = sector3; + CurrentTyre = tyre; + } + public DriverData() + { + DRS = false; + GapToLeader = -1; + LapTime = -1; + Name = "Unknown"; + Position = -1; + Sector1 = -1; + Sector2 = -1; + Sector3 = -1; + CurrentTyre = new Tyre(Tyre.Type.Undefined, -1); + } + /// + /// Method that displays all the data found in a string + /// + /// string containing all the driver datas + public override string ToString() + { + string result = ""; + + //Position + result += "Position : " + Position + Environment.NewLine; + //Gap + result += "GapToLeader : " + Reader.ConvertMsToTime(GapToLeader) + Environment.NewLine; + //LapTime + result += "LapTime : " + Reader.ConvertMsToTime(LapTime) + Environment.NewLine; + //DRS + result += "DRS : " + DRS + Environment.NewLine; + //Tyres + result += "Uses " + CurrentTyre.Coumpound + " tyre " + CurrentTyre.NumberOfLaps + " laps old" + Environment.NewLine; + //Name + result += "DriverName : " + Name + Environment.NewLine; + //Sector 1 + result += "Sector1 : " + Reader.ConvertMsToTime(Sector1) + Environment.NewLine; + //Sector 1 + result += "Sector2 : " + Reader.ConvertMsToTime(Sector2) + Environment.NewLine; + //Sector 1 + result += "Sector3 : " + Reader.ConvertMsToTime(Sector3) + Environment.NewLine; + + return result; + } + } + //Structure to store tyres infos + public struct Tyre + { + //If new tyres were to be added you will have to need to change this enum + public enum Type + { + Soft, + Medium, + Hard, + Inter, + Wet, + Undefined + } + public Type Coumpound; + public int NumberOfLaps; + public Tyre(Type type, int laps) + { + Coumpound = type; + NumberOfLaps = laps; + } + } +} + +``` diff --git a/temp_annexes/Code/DriverDrsWindow.md b/temp_annexes/Code/DriverDrsWindow.md new file mode 100644 index 0000000..d5aa591 --- /dev/null +++ b/temp_annexes/Code/DriverDrsWindow.md @@ -0,0 +1,103 @@ +# DriverDrsWindow.cs + +``` cs +/// Author : Maxime Rohmer +/// Date : 08/05/2023 +/// File : DriverDrsWindow.cs +/// Brief : Window containing DRS related method and infos +/// Version : 0.1 + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Tesseract; + +namespace Test_Merge +{ + internal class DriverDrsWindow:Window + { + private static int EmptyDrsGreenValue = -1; + private static Random rnd = new Random(); + public DriverDrsWindow(Bitmap image, Rectangle bounds,bool generateEngine = true) : base(image, bounds,generateEngine) + { + Name = "DRS"; + } + public override async Task DecodePng() + { + bool result = false; + int greenValue = GetGreenPixels(); + if (EmptyDrsGreenValue == -1) + EmptyDrsGreenValue = greenValue; + + if (greenValue > EmptyDrsGreenValue + EmptyDrsGreenValue / 100 * 30) + result = true; + + return result; + } + private unsafe int GetGreenPixels() + { + int tot = 0; + + Bitmap bmp = WindowImage; + Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height); + BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, bmp.PixelFormat); + int bytesPerPixel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8; + + unsafe + { + byte* ptr = (byte*)bmpData.Scan0.ToPointer(); + for (int y = 0; y < bmp.Height; y++) + { + byte* currentLine = ptr + (y * bmpData.Stride); + for (int x = 0; x < bmp.Width; x++) + { + byte* pixel = currentLine + (x * bytesPerPixel); + + byte blue = pixel[0]; + byte green = pixel[1]; + byte red = pixel[2]; + + if (green > blue * 1.5 && green > red * 1.5) + { + tot++; + } + } + } + } + bmp.UnlockBits(bmpData); + + return tot; + } + public Rectangle GetBox() + { + var tessImage = Pix.LoadFromMemory(ImageToByte(WindowImage)); + Engine.SetVariable("tessedit_char_whitelist", ""); + Page page = Engine.Process(tessImage); + + using (var iter = page.GetIterator()) + { + iter.Begin(); + do + { + Rect boundingBox; + + // Get the bounding box for the current element + if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox)) + { + page.Dispose(); + return new Rectangle(boundingBox.X1, boundingBox.X2, boundingBox.Width, boundingBox.Height); + } + } while (iter.Next(PageIteratorLevel.Word)); + + page.Dispose(); + return new Rectangle(0, 0, 0, 0); + } + } + } +} + +``` diff --git a/temp_annexes/Code/DriverGapToLeaderWindow.md b/temp_annexes/Code/DriverGapToLeaderWindow.md new file mode 100644 index 0000000..dea25f4 --- /dev/null +++ b/temp_annexes/Code/DriverGapToLeaderWindow.md @@ -0,0 +1,37 @@ +# DriverGapToLeaderWindow.cs + +``` cs +/// Author : Maxime Rohmer +/// Date : 08/05/2023 +/// File : DriverGapToLeaderWindow.cs +/// Brief : Window containing infos about the gap to the leader of a driver +/// Version : 0.1 + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Test_Merge +{ + internal class DriverGapToLeaderWindow:Window + { + public DriverGapToLeaderWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) + { + Name = "GapToLeader"; + } + /// + /// Decodes the gap to leader using Tesseract OCR + /// + /// + public override async Task DecodePng() + { + int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Gap, Engine); + return result; + } + } +} + +``` diff --git a/temp_annexes/Code/DriverLapTimeWindow.md b/temp_annexes/Code/DriverLapTimeWindow.md new file mode 100644 index 0000000..4281842 --- /dev/null +++ b/temp_annexes/Code/DriverLapTimeWindow.md @@ -0,0 +1,37 @@ +# DriverLapTimeWindow.cs + +``` cs +/// Author : Maxime Rohmer +/// Date : 08/05/2023 +/// File : DriverLapTimeWindow +/// Brief : Window containing infos about the lap time of a driver +/// Version : 0.1 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Drawing; + +namespace Test_Merge +{ + internal class DriverLapTimeWindow:Window + { + public DriverLapTimeWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) + { + Name = "LapTime"; + } + /// + /// Decodes the lap time contained in the image using OCR Tesseract + /// + /// The laptime in int (ms) + public override async Task DecodePng() + { + int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.LapTime, Engine); + return result; + } + } +} + +``` diff --git a/temp_annexes/Code/DriverNameWindow.md b/temp_annexes/Code/DriverNameWindow.md new file mode 100644 index 0000000..153a87b --- /dev/null +++ b/temp_annexes/Code/DriverNameWindow.md @@ -0,0 +1,63 @@ +# DriverNameWindow.cs + +``` cs +/// Author : Maxime Rohmer +/// Date : 08/05/2023 +/// File : DriverNameWindow +/// Brief : Window containing infos about the name of the driver +/// Version : 0.1 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Drawing; + +namespace Test_Merge +{ + public class DriverNameWindow : Window + { + public static Random rnd = new Random(); + public DriverNameWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) + { + Name = "Name"; + } + /// + /// Decodes using OCR wich driver name is in the image + /// + /// + /// The driver name in string + public override async Task DecodePng(List DriverList) + { + string result = ""; + result = await GetStringFromPng(WindowImage, Engine); + + if (!IsADriver(DriverList, result)) + { + //I put everything in uppercase to try to lower the chances of bad answers + result = FindClosestMatch(DriverList.ConvertAll(d => d.ToUpper()), result.ToUpper()); + } + return result; + } + /// + /// Verifies that the name found in the OCR is a valid name + /// + /// + /// + /// If ye or no the driver exists + private static bool IsADriver(List driverList, string potentialDriver) + { + bool result = false; + //I cant use drivers.Contains because it has missmatched cases and all + foreach (string name in driverList) + { + if (name.ToUpper() == potentialDriver.ToUpper()) + result = true; + } + return result; + } + } +} + +``` diff --git a/temp_annexes/Code/DriverPositionWindow.md b/temp_annexes/Code/DriverPositionWindow.md new file mode 100644 index 0000000..74ce045 --- /dev/null +++ b/temp_annexes/Code/DriverPositionWindow.md @@ -0,0 +1,47 @@ +# DriverPositionWindow.cs + +``` cs +/// Author : Maxime Rohmer +/// Date : 08/05/2023 +/// File : DriverPosition.cs +/// Brief : Window containing infos about the position of a driver. +/// Version : 0.1 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Drawing; + +namespace Test_Merge +{ + public class DriverPositionWindow:Window + { + public DriverPositionWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) + { + Name = "Position"; + } + /// + /// Decodes the position number using Tesseract OCR + /// + /// The position of the pilot in int + public override async Task DecodePng() + { + string ocrResult = await GetStringFromPng(WindowImage, Engine, "0123456789"); + + int position; + try + { + position = Convert.ToInt32(ocrResult); + } + catch + { + position = -1; + } + return position; + } + } +} + +``` diff --git a/temp_annexes/Code/DriverSectorWindow.md b/temp_annexes/Code/DriverSectorWindow.md new file mode 100644 index 0000000..dc342d7 --- /dev/null +++ b/temp_annexes/Code/DriverSectorWindow.md @@ -0,0 +1,37 @@ +# DriverSectorWindow.cs + +``` cs +/// Author : Maxime Rohmer +/// Date : 08/05/2023 +/// File : DriverSectorWindow.cs +/// Brief : Window containing infos about a driver sector time. Can be the first second or third, does not matter. +/// Version : 0.1 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Drawing; + +namespace Test_Merge +{ + internal class DriverSectorWindow:Window + { + public DriverSectorWindow(Bitmap image, Rectangle bounds, int sectorId, bool generateEngine = true) : base(image, bounds,generateEngine) + { + Name = "Sector"+sectorId; + } + /// + /// Decodes the sector + /// + /// the sector time in int (ms) + public override async Task DecodePng() + { + int ocrResult = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector, Engine); + return ocrResult; + } + } +} + +``` diff --git a/temp_annexes/Code/DriverTyresWindow.md b/temp_annexes/Code/DriverTyresWindow.md new file mode 100644 index 0000000..485c438 --- /dev/null +++ b/temp_annexes/Code/DriverTyresWindow.md @@ -0,0 +1,146 @@ +# DriverTyresWindow.cs + +``` cs +/// Author : Maxime Rohmer +/// Date : 08/05/2023 +/// File : DriverTyresWindow.cs +/// Brief : Window containing infos about a driver's tyre +/// Version : 0.1 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Drawing; + +namespace Test_Merge +{ + public class DriverTyresWindow:Window + { + private static Random rnd = new Random(); + int seed = rnd.Next(0, 10000); + + //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); + public static Color HARD_TYRE_COLOR = Color.FromArgb(0xa4, 0xa5, 0xa8); + public static Color INTER_TYRE_COLOR = Color.FromArgb(0x00, 0xa4, 0x2e); + public static Color WET_TYRE_COLOR = Color.FromArgb(0x27, 0x60, 0xa6); + public static Color EMPTY_COLOR = Color.FromArgb(0x20, 0x20, 0x20); + + public DriverTyresWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) + { + Name = "Tyres"; + } + /// + /// This will decode the content of the image + /// + /// And object containing what was on the image + public override async Task DecodePng() + { + return await GetTyreInfos(); + } + /// + /// Method that will decode whats on the image and return the tyre infos it could manage to recover + /// + /// A tyre object containing tyre infos + private async Task GetTyreInfos() + { + Bitmap tyreZone = GetSmallBitmapFromBigOne(WindowImage, FindTyreZone()); + Tyre.Type type = Tyre.Type.Undefined; + type = GetTyreTypeFromColor(OcrImage.GetAvgColorFromBitmap(tyreZone)); + int laps = -1; + + string number = await GetStringFromPng(tyreZone, Engine, "0123456789", OcrImage.WindowType.Tyre); + try + { + laps = Convert.ToInt32(number); + } + catch + { + //We could not convert the number so its a letter so its 0 laps old + laps = 0; + } + //tyreZone.Save(Reader.DEBUG_DUMP_FOLDER + "Tyre" + type + "Laps" + laps + '#' + rnd.Next(0, 1000) + ".png"); + return new Tyre(type, laps); + } + /// + /// Finds where the important part of the image is + /// + /// A rectangle containing position and dimensions of the important part of the image + private Rectangle FindTyreZone() + { + Bitmap bmp = WindowImage; + int currentPosition = bmp.Width; + int height = bmp.Height / 2; + Color limitColor = Color.FromArgb(0x50, 0x50, 0x50); + Color currentColor = Color.FromArgb(0, 0, 0); + + Size newWindowSize = new Size(bmp.Height - Convert.ToInt32((float)bmp.Height / 100f * 25f), bmp.Height - Convert.ToInt32((float)bmp.Height / 100f * 35f)); + + while (currentColor.R <= limitColor.R && currentColor.G <= limitColor.G && currentColor.B <= limitColor.B && currentPosition > 0) + { + currentPosition--; + currentColor = bmp.GetPixel(currentPosition, height); + } + + //Its here to let the new window include a little bit of the right + int CorrectedX = currentPosition - (newWindowSize.Width) + Convert.ToInt32((float)newWindowSize.Width / 100f * 10f); + int CorrectedY = Convert.ToInt32((float)newWindowSize.Height / 100f * 35f); + if (CorrectedX <= 0) + return new Rectangle(0, 0, newWindowSize.Width, newWindowSize.Height); + + return new Rectangle(CorrectedX, CorrectedY, newWindowSize.Width, newWindowSize.Height); + } + //This method has been created with the help of chatGPT + /// + /// Methods that compares a list of colors to see wich is the closest from the input color and decide wich tyre type it is + /// + /// The color that you found + /// The tyre type + public Tyre.Type GetTyreTypeFromColor(Color inputColor) + { + Tyre.Type type = Tyre.Type.Undefined; + List colors = new List(); + //dont forget that if for some reason someday F1 adds a new Tyre type you will need to add it in the constants but also here in the list + //You will also need to add it below in the Tyre object's enum and add an if in the end of this method + colors.Add(SOFT_TYRE_COLOR); + colors.Add(MEDIUM_TYRE_COLOR); + colors.Add(HARD_TYRE_COLOR); + colors.Add(INTER_TYRE_COLOR); + colors.Add(WET_TYRE_COLOR); + colors.Add(EMPTY_COLOR); + + Color closestColor = colors[0]; + int closestDistance = int.MaxValue; + foreach (Color color in colors) + { + int distance = Math.Abs(color.R - inputColor.R) + Math.Abs(color.G - inputColor.G) + Math.Abs(color.B - inputColor.B); + if (distance < closestDistance) + { + closestColor = color; + closestDistance = distance; + } + } + + //We cant use a switch as the colors cant be constants ... + if (closestColor == SOFT_TYRE_COLOR) + type = Tyre.Type.Soft; + if (closestColor == MEDIUM_TYRE_COLOR) + type = Tyre.Type.Medium; + if (closestColor == HARD_TYRE_COLOR) + type = Tyre.Type.Hard; + if (closestColor == INTER_TYRE_COLOR) + type = Tyre.Type.Inter; + if (closestColor == WET_TYRE_COLOR) + type = Tyre.Type.Wet; + if (closestColor == EMPTY_COLOR) + return Tyre.Type.Undefined; + + return type; + } + } +} + +``` diff --git a/temp_annexes/Code/F1TVEmulator.md b/temp_annexes/Code/F1TVEmulator.md new file mode 100644 index 0000000..d21be84 --- /dev/null +++ b/temp_annexes/Code/F1TVEmulator.md @@ -0,0 +1,293 @@ +# F1TVEmulator.cs + +``` cs +/// Author : Maxime Rohmer +/// Date : 08/05/2023 +/// File : F1TVEmulator.cs +/// Brief : Class that contains methods to emulate a browser and navigate the F1TV website +/// Version : 0.1 + +using OpenQA.Selenium; +using OpenQA.Selenium.Firefox; +using OpenQA.Selenium.Interactions; +using OpenQA.Selenium.Support.UI; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Test_Merge +{ + internal class F1TVEmulator + { + public const string COOKIE_HOST = ".formula1.com"; + public const string PYTHON_COOKIE_RETRIEVAL_FILENAME = "recoverCookiesCSV.py"; + public const string GECKODRIVER_FILENAME = @"geckodriver-v0.27.0-win64\geckodriver.exe"; + //BE CAREFULL IF YOU CHANGE IT HERE YOU NEED TO CHANGE IT IN THE PYTHON SCRIPT TOO + public const string COOKIES_CSV_FILENAME = "cookies.csv"; + + private FirefoxDriver Driver; + + private bool _ready; + private string _grandPrixUrl; + public string GrandPrixUrl { get => _grandPrixUrl; private set => _grandPrixUrl = value; } + public bool Ready { get => _ready; set => _ready = value; } + public F1TVEmulator(string grandPrixUrl) + { + GrandPrixUrl = grandPrixUrl; + Ready = false; + } + private void StartCookieRecovering() + { + string scriptPath = PYTHON_COOKIE_RETRIEVAL_FILENAME; + Process process = new Process(); + process.StartInfo.FileName = "python.exe"; + process.StartInfo.Arguments = scriptPath; + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.Start(); + string output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + } + public string GetCookie(string host, string name) + { + StartCookieRecovering(); + string value = ""; + List cookies = new List(); + using (var reader = new StreamReader(COOKIES_CSV_FILENAME)) + { + // Read the header row and validate column order + string header = reader.ReadLine(); + string[] expectedColumns = { "host_key", "name", "value", "path", "expires_utc", "is_secure", "is_httponly" }; + string[] actualColumns = header.Split(','); + for (int i = 0; i < expectedColumns.Length; i++) + { + if (expectedColumns[i] != actualColumns[i]) + { + throw new InvalidOperationException($"Expected column '{expectedColumns[i]}' at index {i} but found '{actualColumns[i]}'"); + } + } + + // Read each data row and parse values into a Cookie object + while (!reader.EndOfStream) + { + string line = reader.ReadLine(); + string[] fields = line.Split(','); + + string hostname = fields[0]; + string cookieName = fields[1]; + + if (hostname == host && cookieName == name) + { + value = fields[2]; + } + } + } + + return value; + } + public async Task Start() + { + Ready = false; + + string loginCookieName = "login"; + string loginSessionCookieName = "login-session"; + string loginCookieValue = GetCookie(COOKIE_HOST, loginCookieName); + string loginSessionValue = GetCookie(COOKIE_HOST, loginSessionCookieName); + + int windowWidth = 1920; + int windowHeight = 768; + + var service = FirefoxDriverService.CreateDefaultService(GECKODRIVER_FILENAME); + service.Host = "127.0.0.1"; + service.Port = 5555; + + FirefoxProfile profile = new FirefoxProfile(); + FirefoxOptions options = new FirefoxOptions(); + //profile.SetPreference("full-screen-api.ignore-widgets", true); + //profile.SetPreference("media.hardware-video-decoding.enabled", true); + //profile.SetPreference("full-screen-api.enabled", true); + options.Profile = profile; + profile.SetPreference("layout.css.devPixelsPerPx", "1.0"); + + options.AcceptInsecureCertificates = true; + options.AddArgument("--headless"); + //options.AddArgument("--start-maximized"); + //options.AddArgument("--window-size=1920x1080"); + //options.AddArgument("--width=" + windowWidth); + //options.AddArgument("--height=" + windowHeight); + //options.AddArgument("-window-size=1920x1080"); + //options.AddArgument("--width=1920"); + //options.AddArgument("--height=1080"); + //profile + + try + { + Driver = new FirefoxDriver(service, options); + } + catch + { + Ready = false; + return 101; + } + + Actions actions = new Actions(Driver); + var loginCookie = new Cookie(loginCookieName, loginCookieValue, COOKIE_HOST, "/", DateTime.Now.AddDays(5)); + var loginSessionCookie = new Cookie(loginSessionCookieName, loginSessionValue, COOKIE_HOST, "/", DateTime.Now.AddDays(5)); + + Driver.Navigate().GoToUrl("https://f1tv.formula1.com/"); + + Driver.Manage().Cookies.AddCookie(loginCookie); + Driver.Manage().Cookies.AddCookie(loginSessionCookie); + + try + { + Driver.Navigate().GoToUrl(GrandPrixUrl); + } + catch + { + //The url is not a valid url + Driver.Dispose(); + return 103; + } + + //Waits for the page to fully load + Driver.Manage().Timeouts().PageLoad = TimeSpan.FromSeconds(30); + + //Removes the cookie prompt + try + { + IWebElement conscentButton = Driver.FindElement(By.Id("truste-consent-button")); + conscentButton.Click(); + } + catch + { + //Could not locate the cookie button + Screenshot("ERROR104"); + Driver.Dispose(); + return 104; + } + + //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); + + //Switches to the Data channel + try + { + IWebElement dataChannelButton = Driver.FindElement(By.ClassName("data-button")); + dataChannelButton.Click(); + } + catch + { + //If the data button does not exists its because the user is not connected + Screenshot("ERROR102"); + Driver.Dispose(); + return 102; + } + + //Open settings + // Press the space key, this should make the setting button visible + // It does not matter if the feed is paused because when changing channel it autoplays + actions.SendKeys(OpenQA.Selenium.Keys.Space).Perform(); + //Clicks on the settings Icon + + int tries = 0; + bool success = false; + while (tries < 100 && !success) + { + Thread.Sleep(100); + try + { + IWebElement settingsButton = Driver.FindElement(By.ClassName("bmpui-ui-settingstogglebutton")); + settingsButton.Click(); + IWebElement selectElement = Driver.FindElement(By.ClassName("bmpui-ui-videoqualityselectbox")); + SelectElement select = new SelectElement(selectElement); + IWebElement selectOption = selectElement.FindElement(By.CssSelector("option[value^='1080_']")); + selectOption.Click(); + success = true; + } + catch + { + //Sometimes it can crash because it could not get the options to show up in time. When it happens just retry + success = false; + tries++; + } + } + + if (!success) + { + Screenshot("ERROR105"); + Driver.Dispose(); + return 105; + } + + Screenshot("BEFOREFULLSCREEN"); + + //Makes the feed fullscreen + //Driver.Manage().Window.Size = new System.Drawing.Size(windowWidth, windowHeight); + Driver.Manage().Window.Maximize(); + WebDriverWait wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); + try + { + IWebElement fullScreenButton = Driver.FindElement(By.ClassName("bmpui-ui-fullscreentogglebutton")); + fullScreenButton.Click(); + } + catch + { + Screenshot("ERROR106"); + Driver.Dispose(); + return 106; + } + + Screenshot("AFTERFULLSCREEN"); + + //STARTUP FINISHED READY TO SCREENSHOT + Ready = true; + return 0; + } + public Bitmap Screenshot(string name = "TEST") + { + Bitmap result = new Bitmap(4242, 6969); + try + { + //Screenshot scrsht = ((ITakesScreenshot)Driver).GetScreenshot(); + //profileriver.SetPreference("layout.css.devPixelsPerPx", "1.0"); + + //Screenshot scrsht = Driver.GetFullPageScreenshot(); + Screenshot scrsht = Driver.GetScreenshot(); + + + byte[] screenshotBytes = Convert.FromBase64String(scrsht.AsBase64EncodedString); + MemoryStream stream = new MemoryStream(screenshotBytes); + + result = new Bitmap(stream); + //result.Save(name + ".png"); + scrsht.SaveAsFile(name + ".png"); + } + catch + { + //Nothing for now + } + return result; + } + public void Stop() + { + Ready = false; + Driver.Dispose(); + } + public void ResetDriver() + { + Ready = false; + Driver.Dispose(); + Driver = null; + } + } +} + +``` diff --git a/temp_annexes/Code/Form1.md b/temp_annexes/Code/Form1.md new file mode 100644 index 0000000..92eb895 --- /dev/null +++ b/temp_annexes/Code/Form1.md @@ -0,0 +1,32 @@ +# Form1.cs + +``` cs +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace Test_Merge +{ + public partial class Form1 : Form + { + public Form1() + { + InitializeComponent(); + } + + private void btnSettings_Click(object sender, EventArgs e) + { + Settings settingsForm = new Settings(); + settingsForm.ShowDialog(); + MessageBox.Show(settingsForm.GrandPrixUrl + Environment.NewLine + settingsForm.GrandPrixName + Environment.NewLine + settingsForm.GrandPrixYear); + } + } +} + +``` diff --git a/temp_annexes/Code/OcrImage.md b/temp_annexes/Code/OcrImage.md new file mode 100644 index 0000000..f3bb55b --- /dev/null +++ b/temp_annexes/Code/OcrImage.md @@ -0,0 +1,544 @@ +# OcrImage.cs + +``` cs +/// Author : Maxime Rohmer +/// Date : 08/05/2023 +/// File : OcrImage.cs +/// Brief : Class containing all the methods used to enhance images for OCR +/// Version : 0.1 + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; + +namespace Test_Merge +{ + public class OcrImage + { + //this is a hardcoded value based on the colors of the F1TV data channel background you can change it if sometime in the future the color changes + //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; + public enum WindowType + { + LapTime, + Text, + Sector, + Gap, + Tyre, + } + + /// + /// Create a new Ocr image to help enhance the given bitmap for OCR + /// + /// The image you want to enhance + public OcrImage(Bitmap inputBitmap) + { + InputBitmap = inputBitmap; + } + /// + /// Enhances the image depending on wich type of window the image comes from + /// + /// The type of the window. Depending on it different enhancing features will be applied + /// The enhanced Bitmap + public Bitmap Enhance(WindowType type = WindowType.Text) + { + Bitmap outputBitmap = (Bitmap)InputBitmap.Clone(); + switch (type) + { + case WindowType.LapTime: + outputBitmap = Tresholding(outputBitmap, 185); + outputBitmap = Resize(outputBitmap, 2); + outputBitmap = Dilatation(outputBitmap, 1); + outputBitmap = Erode(outputBitmap, 1); + break; + case WindowType.Text: + outputBitmap = InvertColors(outputBitmap); + outputBitmap = Tresholding(outputBitmap, 165); + outputBitmap = Resize(outputBitmap, 2); + outputBitmap = Dilatation(outputBitmap, 1); + break; + case WindowType.Tyre: + outputBitmap = RemoveUseless(outputBitmap); + outputBitmap = Resize(outputBitmap, 4); + outputBitmap = Dilatation(outputBitmap, 1); + break; + default: + outputBitmap = Tresholding(outputBitmap, 165); + outputBitmap = Resize(outputBitmap, 4); + outputBitmap = Erode(outputBitmap, 1); + break; + } + return outputBitmap; + } + /// + /// Method that convert a colored RGB bitmap into a GrayScale image + /// + /// The Bitmap you want to convert + /// The bitmap in grayscale + 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); + int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8; + + unsafe + { + 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); + + byte blue = pixel[0]; + byte green = pixel[1]; + byte red = pixel[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); + + pixel[0] = pixel[1] = pixel[2] = (byte)gray; + } + } + } + inputBitmap.UnlockBits(bmpData); + + return inputBitmap; + } + /// + /// Method that binaries the input image up to a certain treshold given + /// + /// the bitmap you want to convert to binary colors + /// The floor at wich the color is considered as white or black + /// The binarised bitmap + public static Bitmap Tresholding(Bitmap inputBitmap, int threshold) + { + 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 + { + byte* ptr = (byte*)bmpData.Scan0.ToPointer(); + int bmpHeight = inputBitmap.Height; + int bmpWidth = inputBitmap.Width; + Parallel.For(0, bmpHeight, y => + { + byte* currentLine = ptr + (y * bmpData.Stride); + for (int x = 0; x < bmpWidth; x++) + { + byte* pixel = currentLine + (x * bytesPerPixel); + + byte blue = pixel[0]; + byte green = pixel[1]; + byte red = pixel[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); + int value = gray < threshold ? 0 : 255; + + pixel[0] = pixel[1] = pixel[2] = (byte)value; + } + }); + } + inputBitmap.UnlockBits(bmpData); + + return inputBitmap; + } + /// + /// Method that removes the pixels that are flagged as background + /// + /// The bitmap you want to remove the background from + /// The Bitmap without the background + public static Bitmap RemoveBG(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 + { + 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); + + int B = pixel[0]; + int G = pixel[1]; + int R = pixel[2]; + + 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); + + return inputBitmap; + } + /// + /// Method that removes all the useless things from the image and returns hopefully only the numbers + /// + /// The bitmap you want to remove useless things from (Expects a cropped part of the TyreWindow) + /// The bitmap with (hopefully) only the digits + public unsafe static Bitmap RemoveUseless(Bitmap inputBitmap) + { + //Note you can use something else than a cropped tyre window but I would recommend checking the code first to see if it fits your intended use + 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; + + byte* ptr = (byte*)bmpData.Scan0.ToPointer(); + for (int y = 0; y < inputBitmap.Height; y++) + { + byte* currentLine = ptr + (y * bmpData.Stride); + + List pixelsToRemove = new List(); + + bool fromBorder = true; + + for (int x = 0; x < inputBitmap.Width; x++) + { + byte* pixel = currentLine + (x * bytesPerPixel); + + int B = pixel[0]; + int G = pixel[1]; + int R = pixel[2]; + + if (fromBorder && B < F1TV_BACKGROUND_TRESHOLD.B && G < F1TV_BACKGROUND_TRESHOLD.G && R < F1TV_BACKGROUND_TRESHOLD.R) + { + pixelsToRemove.Add(x); + } + else + { + if (fromBorder) + { + fromBorder = false; + pixelsToRemove.Add(x); + } + } + } + fromBorder = true; + for (int x = inputBitmap.Width - 1; x > 0; x--) + { + byte* pixel = currentLine + (x * bytesPerPixel); + + int B = pixel[0]; + int G = pixel[1]; + int R = pixel[2]; + + if (fromBorder && B < F1TV_BACKGROUND_TRESHOLD.B && G < F1TV_BACKGROUND_TRESHOLD.G && R < F1TV_BACKGROUND_TRESHOLD.R) + { + pixelsToRemove.Add(x); + } + else + { + if (fromBorder) + { + fromBorder = false; + pixelsToRemove.Add(x); + } + } + } + + foreach (int pxPos in pixelsToRemove) + { + byte* pixel = currentLine + (pxPos * bytesPerPixel); + + pixel[0] = 0xFF; + pixel[1] = 0xFF; + pixel[2] = 0xFF; + } + } + + //Removing the color parts + 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); + + int B = pixel[0]; + int G = pixel[1]; + int R = pixel[2]; + + if (R >= F1TV_BACKGROUND_TRESHOLD.R + 15 || G >= F1TV_BACKGROUND_TRESHOLD.G + 15 || B >= F1TV_BACKGROUND_TRESHOLD.B + 15) + { + pixel[0] = 0xFF; + pixel[1] = 0xFF; + pixel[2] = 0xFF; + } + } + } + + inputBitmap.UnlockBits(bmpData); + return inputBitmap; + } + /// + /// Recovers the average colors from the Image. NOTE : It wont take in account colors that are lower than the background + /// + /// The bitmap you want to get the average color from + /// The average color of the bitmap + public static Color GetAvgColorFromBitmap(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; + + int totR = 0; + int totG = 0; + int totB = 0; + + int totPixels = 1; + + unsafe + { + byte* ptr = (byte*)bmpData.Scan0.ToPointer(); + int bmpHeight = inputBitmap.Height; + int bmpWidth = inputBitmap.Width; + Parallel.For(0, bmpHeight, y => + { + byte* currentLine = ptr + (y * bmpData.Stride); + for (int x = 0; x < bmpWidth; x++) + { + byte* pixel = currentLine + (x * bytesPerPixel); + + int B = pixel[0]; + int G = pixel[1]; + int R = pixel[2]; + + if (R >= F1TV_BACKGROUND_TRESHOLD.R || G >= F1TV_BACKGROUND_TRESHOLD.G || B >= F1TV_BACKGROUND_TRESHOLD.B) + { + totPixels++; + totB += pixel[0]; + totG += pixel[1]; + totR += pixel[2]; + } + } + }); + } + inputBitmap.UnlockBits(bmpData); + + return Color.FromArgb(255, Convert.ToInt32((float)totR / (float)totPixels), Convert.ToInt32((float)totG / (float)totPixels), Convert.ToInt32((float)totB / (float)totPixels)); + } + /// + /// This method simply inverts all the colors in a Bitmap + /// + /// the bitmap you want to invert the colors from + /// The bitmap with inverted colors + public static Bitmap InvertColors(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 + { + 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); + + pixel[0] = (byte)(255 - pixel[0]); + pixel[1] = (byte)(255 - pixel[1]); + pixel[2] = (byte)(255 - pixel[2]); + } + } + } + inputBitmap.UnlockBits(bmpData); + + return inputBitmap; + } + /// + /// Methods that applies Bicubic interpolation to increase the size and resolution of an image + /// + /// The bitmap you want to resize + /// The factor of resizing you want to use. I recommend using even numbers + /// The bitmap witht the new size + public static Bitmap Resize(Bitmap inputBitmap, int resizeFactor) + { + var resultBitmap = new Bitmap(inputBitmap.Width * resizeFactor, inputBitmap.Height * resizeFactor); + + using (var graphics = Graphics.FromImage(resultBitmap)) + { + graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; + graphics.DrawImage(inputBitmap, new Rectangle(0, 0, resultBitmap.Width, resultBitmap.Height)); + } + + return resultBitmap; + } + /// + /// method that Highlights the countours of a Bitmap + /// + /// The bitmap you want to highlight the countours of + /// The bitmap with countours highlighted + public static Bitmap HighlightContours(Bitmap inputBitmap) + { + Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); + + Bitmap grayscale = Grayscale(inputBitmap); + Bitmap thresholded = Tresholding(grayscale, 128); + Bitmap dilated = Dilatation(thresholded, 3); + Bitmap eroded = Erode(dilated, 3); + + for (int y = 0; y < inputBitmap.Height; y++) + { + for (int x = 0; x < inputBitmap.Width; x++) + { + Color pixel = inputBitmap.GetPixel(x, y); + Color dilatedPixel = dilated.GetPixel(x, y); + Color erodedPixel = eroded.GetPixel(x, y); + + int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); + int threshold = dilatedPixel.R; + + if (gray > threshold) + { + outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); + } + else if (gray <= threshold && erodedPixel.R == 0) + { + outputBitmap.SetPixel(x, y, Color.FromArgb(255, 0, 0)); + } + else + { + outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); + } + } + } + + return outputBitmap; + } + /// + /// Method that that erodes the morphology of a bitmap + /// + /// The bitmap you want to erode + /// The amount of Erosion you want (be carefull its expensive on ressources) + /// The Bitmap with the eroded contents + public static Bitmap Erode(Bitmap inputBitmap, int kernelSize) + { + Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); + + int[,] kernel = new int[kernelSize, kernelSize]; + + for (int i = 0; i < kernelSize; i++) + { + for (int j = 0; j < kernelSize; j++) + { + kernel[i, j] = 1; + } + } + + for (int y = kernelSize / 2; y < inputBitmap.Height - kernelSize / 2; y++) + { + for (int x = kernelSize / 2; x < inputBitmap.Width - kernelSize / 2; x++) + { + bool flag = true; + + for (int i = -kernelSize / 2; i <= kernelSize / 2; i++) + { + for (int j = -kernelSize / 2; j <= kernelSize / 2; j++) + { + Color pixel = inputBitmap.GetPixel(x + i, y + j); + int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); + + if (gray >= 128 && kernel[i + kernelSize / 2, j + kernelSize / 2] == 1) + { + flag = false; + break; + } + } + + if (!flag) + { + break; + } + } + + if (flag) + { + outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); + } + else + { + outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); + } + } + } + + return outputBitmap; + } + /// + /// Method that that use dilatation of the morphology of a bitmap + /// + /// The bitmap you want to use dilatation on + /// The amount of dilatation you want (be carefull its expensive on ressources) + /// The Bitmap after Dilatation + public static Bitmap Dilatation(Bitmap inputBitmap, int kernelSize) + { + Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); + + int[,] kernel = new int[kernelSize, kernelSize]; + + for (int i = 0; i < kernelSize; i++) + { + for (int j = 0; j < kernelSize; j++) + { + kernel[i, j] = 1; + } + } + + for (int y = kernelSize / 2; y < inputBitmap.Height - kernelSize / 2; y++) + { + for (int x = kernelSize / 2; x < inputBitmap.Width - kernelSize / 2; x++) + { + bool flag = false; + + for (int i = -kernelSize / 2; i <= kernelSize / 2; i++) + { + for (int j = -kernelSize / 2; j <= kernelSize / 2; j++) + { + Color pixel = inputBitmap.GetPixel(x + i, y + j); + int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); + + if (gray < 128 && kernel[i + kernelSize / 2, j + kernelSize / 2] == 1) + { + flag = true; + break; + } + } + + if (flag) + { + break; + } + } + + if (flag) + { + outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); + } + else + { + outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); + } + } + } + + return outputBitmap; + } + } +} + +``` diff --git a/temp_annexes/Code/Program.md b/temp_annexes/Code/Program.md new file mode 100644 index 0000000..e269b1e --- /dev/null +++ b/temp_annexes/Code/Program.md @@ -0,0 +1,27 @@ +# Program.cs + +``` cs +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace Test_Merge +{ + internal static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new Form1()); + } + } +} + +``` diff --git a/temp_annexes/Code/Reader.md b/temp_annexes/Code/Reader.md new file mode 100644 index 0000000..407e0f4 --- /dev/null +++ b/temp_annexes/Code/Reader.md @@ -0,0 +1,235 @@ +# Reader.cs + +``` cs +/// Author : Maxime Rohmer +/// Date : 08/05/2023 +/// File : Reader.cs +/// Brief : Class used to Read the config file for the OCR +/// Version : 0.1 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Drawing; +using System.Windows.Forms; +using System.IO; +using System.Text.Json; + +namespace Test_Merge +{ + public class Reader + { + const int NUMBER_OF_DRIVERS = 20; + public List Drivers; + public List MainZones; + + public Reader(string configFile, Bitmap image,bool loadOCR = true) + { + MainZones = Load(image,configFile,ref Drivers,loadOCR); + } + /// + /// Method that reads the JSON config file and create all the Zones and Windows + /// + /// The image #id on wich you want to create the zones on + public static List Load(Bitmap image,string configFilePath,ref List driverListToFill,bool LoadOCR) + { + List mainZones = new List(); + Bitmap fullImage = image; + List drivers; + Zone mainZone; + + try + { + using (var streamReader = new StreamReader(configFilePath)) + { + var jsonText = streamReader.ReadToEnd(); + var jsonDocument = JsonDocument.Parse(jsonText); + + var driversNames = jsonDocument.RootElement.GetProperty("Drivers"); + driverListToFill = new List(); + + foreach (var nameElement in driversNames.EnumerateArray()) + { + driverListToFill.Add(nameElement.GetString()); + } + + var mainProperty = jsonDocument.RootElement.GetProperty("Main"); + Point MainPosition = new Point(mainProperty.GetProperty("x").GetInt32(), mainProperty.GetProperty("y").GetInt32()); + Size MainSize = new Size(mainProperty.GetProperty("width").GetInt32(), mainProperty.GetProperty("height").GetInt32()); + Rectangle MainRectangle = new Rectangle(MainPosition, MainSize); + mainZone = new Zone(image, MainRectangle,"Main"); + + var zones = mainProperty.GetProperty("Zones"); + var driverZone = zones[0].GetProperty("DriverZone"); + + Point FirstZonePosition = new Point(driverZone.GetProperty("x").GetInt32(), driverZone.GetProperty("y").GetInt32()); + Size FirstZoneSize = new Size(driverZone.GetProperty("width").GetInt32(), driverZone.GetProperty("height").GetInt32()); + + var windows = driverZone.GetProperty("Windows"); + + var driverPosition = windows[0].GetProperty("Position"); + Size driverPositionArea = new Size(driverPosition.GetProperty("width").GetInt32(), FirstZoneSize.Height); + Point driverPositionPosition = new Point(driverPosition.GetProperty("x").GetInt32(), driverPosition.GetProperty("y").GetInt32()); + + var driverGapToLeader = windows[0].GetProperty("GapToLeader"); + Size driverGapToLeaderArea = new Size(driverGapToLeader.GetProperty("width").GetInt32(), FirstZoneSize.Height); + Point driverGapToLeaderPosition = new Point(driverGapToLeader.GetProperty("x").GetInt32(), driverGapToLeader.GetProperty("y").GetInt32()); + + var driverLapTime = windows[0].GetProperty("LapTime"); + Size driverLapTimeArea = new Size(driverLapTime.GetProperty("width").GetInt32(), FirstZoneSize.Height); + Point driverLapTimePosition = new Point(driverLapTime.GetProperty("x").GetInt32(), driverLapTime.GetProperty("y").GetInt32()); + + + var driverDrs = windows[0].GetProperty("DRS"); + Size driverDrsArea = new Size(driverDrs.GetProperty("width").GetInt32(), FirstZoneSize.Height); + Point driverDrsPosition = new Point(driverDrs.GetProperty("x").GetInt32(), driverDrs.GetProperty("y").GetInt32()); + + var driverTyres = windows[0].GetProperty("Tyres"); + Size driverTyresArea = new Size(driverTyres.GetProperty("width").GetInt32(), FirstZoneSize.Height); + Point driverTyresPosition = new Point(driverTyres.GetProperty("x").GetInt32(), driverTyres.GetProperty("y").GetInt32()); + + var driverName = windows[0].GetProperty("Name"); + Size driverNameArea = new Size(driverName.GetProperty("width").GetInt32(), FirstZoneSize.Height); + Point driverNamePosition = new Point(driverName.GetProperty("x").GetInt32(), driverName.GetProperty("y").GetInt32()); + + var driverSector1 = windows[0].GetProperty("Sector1"); + Size driverSector1Area = new Size(driverSector1.GetProperty("width").GetInt32(), FirstZoneSize.Height); + Point driverSector1Position = new Point(driverSector1.GetProperty("x").GetInt32(), driverSector1.GetProperty("y").GetInt32()); + + var driverSector2 = windows[0].GetProperty("Sector2"); + Size driverSector2Area = new Size(driverSector2.GetProperty("width").GetInt32(), FirstZoneSize.Height); + Point driverSector2Position = new Point(driverSector2.GetProperty("x").GetInt32(), driverSector2.GetProperty("y").GetInt32()); + + var driverSector3 = windows[0].GetProperty("Sector3"); + Size driverSector3Area = new Size(driverSector3.GetProperty("width").GetInt32(), FirstZoneSize.Height); + Point driverSector3Position = new Point(driverSector3.GetProperty("x").GetInt32(), driverSector3.GetProperty("y").GetInt32()); + + float offset = (((float)mainZone.ZoneImage.Height - (float)(driverListToFill.Count * FirstZoneSize.Height)) / (float)driverListToFill.Count); + Bitmap MainZoneImage = mainZone.ZoneImage; + List zonesToAdd = new List(); + List zonesImages = new List(); + + for (int i = 0; i < NUMBER_OF_DRIVERS; i++) + { + Point tmpPos = new Point(0, FirstZonePosition.Y + i * FirstZoneSize.Height - Convert.ToInt32(i * offset)); + Zone newDriverZone = new Zone(MainZoneImage, new Rectangle(tmpPos, FirstZoneSize), "DriverZone"); + zonesToAdd.Add(newDriverZone); + zonesImages.Add(newDriverZone.ZoneImage); + + newDriverZone.ZoneImage.Save("Driver"+i+".png"); + } + + //Parallel.For(0, NUMBER_OF_DRIVERS, i => + for (int i = 0; i < NUMBER_OF_DRIVERS; i++) + { + Zone newDriverZone = zonesToAdd[(int)i]; + Bitmap zoneImg = zonesImages[(int)i]; + + newDriverZone.AddWindow(new DriverPositionWindow(zoneImg, new Rectangle(driverPositionPosition, driverPositionArea),LoadOCR)); + newDriverZone.AddWindow(new DriverGapToLeaderWindow(zoneImg, new Rectangle(driverGapToLeaderPosition, driverGapToLeaderArea), LoadOCR)); + newDriverZone.AddWindow(new DriverLapTimeWindow(zoneImg, new Rectangle(driverLapTimePosition, driverLapTimeArea), LoadOCR)); + newDriverZone.AddWindow(new DriverDrsWindow(zoneImg, new Rectangle(driverDrsPosition, driverDrsArea), LoadOCR)); + newDriverZone.AddWindow(new DriverTyresWindow(zoneImg, new Rectangle(driverTyresPosition, driverTyresArea), LoadOCR)); + newDriverZone.AddWindow(new DriverNameWindow(zoneImg, new Rectangle(driverNamePosition, driverNameArea), LoadOCR)); + newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector1Position, driverSector1Area),1, LoadOCR)); + newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector2Position, driverSector2Area),2, LoadOCR)); + newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector3Position, driverSector3Area),3, LoadOCR)); + + mainZone.AddZone(newDriverZone); + }//); + //MessageBox.Show("We have a main zone with " + MainZone.Zones.Count() + " Driver zones with " + MainZone.Zones[4].Windows.Count() + " windows each and we have " + Drivers.Count + " drivers"); + mainZones.Add(mainZone); + } + } + catch (IOException ex) + { + MessageBox.Show("Error reading JSON file: " + ex.Message); + } + catch (JsonException ex) + { + MessageBox.Show("Invalid JSON format: " + ex.Message); + } + return mainZones; + } + /// + /// Method that calls all the zones and windows to get the content they can find on the image to display them + /// + /// The id of the image we are working with + /// a string representation of all the returns + public async Task Decode(List mainZones,List drivers) + { + string result = ""; + List mainResults = new List(); + + //Decode + for (int mainZoneId = 0; mainZoneId < mainZones.Count; mainZoneId++) + { + switch (mainZoneId) + { + case 0: + //Main Zone + foreach (Zone z in mainZones[mainZoneId].Zones) + { + mainResults.Add(await z.Decode(Drivers)); + } + break; + //Next there could be a Title Zone and TrackInfoZone + } + } + + //Display + foreach (DriverData driver in mainResults) + { + result += driver.ToString(); + result += Environment.NewLine; + } + + return result; + } + /// + /// Method that can be used to convert an amount of miliseconds into a more readable human form + /// + /// The given amount of miliseconds ton convert + /// A human readable string that represents the ms + public static string ConvertMsToTime(int amountOfMs) + { + //Convert.ToInt32 would round upand I dont want that + int minuts = (int)((float)amountOfMs / (1000f * 60f)); + int seconds = (int)((amountOfMs - (minuts * 60f * 1000f)) / 1000); + int ms = amountOfMs - ((minuts * 60 * 1000) + (seconds * 1000)); + + return minuts + ":" + seconds.ToString("00") + ":" + ms.ToString("000"); + } + /// + /// Old method that can draw on an image where the windows and zones are created. mostly used for debugging + /// + /// the #id of the image we are working with + /// the drawed bitmap + public Bitmap Draw(Bitmap image,List mainZones) + { + + Graphics g = Graphics.FromImage(image); + + foreach (Zone z in mainZones) + { + int count = 0; + foreach (Zone zz in z.Zones) + { + g.DrawRectangle(Pens.Red, z.Bounds); + foreach (Window w in zz.Windows) + { + g.DrawRectangle(Pens.Blue, new Rectangle(z.Bounds.X + zz.Bounds.X, z.Bounds.Y + zz.Bounds.Y, zz.Bounds.Width, zz.Bounds.Height)); + } + + count++; + } + } + + return image; + } + } +} + +``` diff --git a/temp_annexes/Code/Settings.md b/temp_annexes/Code/Settings.md new file mode 100644 index 0000000..38f9dda --- /dev/null +++ b/temp_annexes/Code/Settings.md @@ -0,0 +1,420 @@ +# Settings.cs + +``` cs +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; +using System.IO; + +namespace Test_Merge +{ + public partial class Settings : Form + { + private string _grandPrixUrl = ""; + private string _grandPrixName = ""; + private int _grandPrixYear = 2000; + private List _driverList = new List(); + + private F1TVEmulator Emulator = null; + private ConfigurationTool Config = null; + + private bool CreatingZone = false; + private Point ZoneP1; + private Point ZoneP2; + + private bool CreatingWindow = false; + private Point WindowP1; + private Point WindowP2; + + List WindowsToAdd = new List(); + + public string GrandPrixUrl { get => _grandPrixUrl; private set => _grandPrixUrl = value; } + public string GrandPrixName { get => _grandPrixName; private set => _grandPrixName = value; } + public int GrandPrixYear { get => _grandPrixYear; private set => _grandPrixYear = value; } + public List DriverList { get => _driverList; private set => _driverList = value; } + + public Settings() + { + InitializeComponent(); + Load(); + } + private void Load() + { + RefreshUI(); + } + private void RefreshUI() + { + + lsbDrivers.DataSource = null; + lsbDrivers.DataSource = DriverList; + + if (Directory.Exists(ConfigurationTool.CONFIGS_FOLDER_NAME)) + { + lsbPresets.DataSource = null; + lsbPresets.DataSource = Directory.GetFiles(ConfigurationTool.CONFIGS_FOLDER_NAME); + } + if (CreatingZone) + { + if (ZoneP1 == new Point(-1, -1)) + { + lblZonePointsRemaning.Text = "2 points Remaining"; + } + else + { + lblZonePointsRemaning.Text = "1 point Remaining"; + } + } + else + { + lblZonePointsRemaning.Text = ""; + } + + if (CreatingWindow) + { + if (WindowP1 == new Point(-1, -1)) + { + lblWindowPointsRemaining.Text = "2 points Remaining"; + } + else + { + lblWindowPointsRemaining.Text = "1 point Remaining"; + } + lblWindowPointsRemaining.Text = ConfigurationTool.NUMBER_OF_ZONES - WindowsToAdd.Count() + " Windows remaining"; + } + else + { + lblWindowPointsRemaining.Text = ""; + lblWindowsRemaining.Text = ""; + } + if (Config != null) + { + pbxMain.Image = Config.MainZone.Draw(); + if(Config.MainZone.Zones.Count > 0) + pbxDriverZone.Image = Config.MainZone.Zones[0].Draw(); + } + } + private void CreateNewZone(Point p1, Point p2) + { + Rectangle dimensions = CreateAbsoluteRectangle(p1, p2); + Config = new ConfigurationTool((Bitmap)pbxMain.Image, dimensions); + RefreshUI(); + } + private void CreateWindows(List dimensions) + { + if (Config != null) + { + Config.AddWindows(dimensions); + } + } + private void tbxGpUrl_TextChanged(object sender, EventArgs e) + { + GrandPrixUrl = tbxGpUrl.Text; + } + + private void tbxGpName_TextChanged(object sender, EventArgs e) + { + GrandPrixName = tbxGpName.Text; + } + + private void tbxGpYear_TextChanged(object sender, EventArgs e) + { + int year; + try + { + year = Convert.ToInt32(tbxGpYear.Text); + } + catch + { + year = 1545; + } + GrandPrixYear = year; + } + + private void btnAddDriver_Click(object sender, EventArgs e) + { + string newDriver = tbxDriverName.Text; + DriverList.Add(newDriver); + tbxDriverName.Text = ""; + RefreshUI(); + } + + private void btnRemoveDriver_Click(object sender, EventArgs e) + { + if (lsbDrivers.SelectedIndex >= 0) + { + DriverList.RemoveAt(lsbDrivers.SelectedIndex); + } + RefreshUI(); + } + private void SwitchZoneCreation() + { + if (CreatingZone) + { + CreatingZone = false; + lblZonePointsRemaning.Text = ""; + } + else + { + CreatingZone = true; + + if (Config != null) + Config.ResetMainZone(); + + if (CreatingWindow) + SwitchWindowCreation(); + + if (Emulator != null && Emulator.Ready) + { + Config = null; + pbxMain.Image = Emulator.Screenshot(); + } + + ZoneP1 = new Point(-1, -1); + ZoneP2 = new Point(-1, -1); + + lblZonePointsRemaning.Text = "2 Points left"; + } + RefreshUI(); + } + private void SwitchWindowCreation() + { + if (CreatingWindow) + { + CreatingWindow = false; + } + else + { + CreatingWindow = true; + + if (Config != null) + Config.ResetWindows(); + + if (CreatingZone) + SwitchZoneCreation(); + + WindowP1 = new Point(-1, -1); + WindowP2 = new Point(-1, -1); + + WindowsToAdd = new List(); + } + RefreshUI(); + } + private void btnCreatZone_Click(object sender, EventArgs e) + { + SwitchZoneCreation(); + } + private void btnCreateWindow_Click(object sender, EventArgs e) + { + SwitchWindowCreation(); + } + private void pbxMain_MouseClick(object sender, MouseEventArgs e) + { + if (CreatingZone && pbxMain.Image != null) + { + //Point coordinates = pbxMain.PointToClient(new Point(MousePosition.X, MousePosition.Y)); + Point coordinates = e.Location; + float xOffset = (float)pbxMain.Image.Width / (float)pbxMain.Width; + float yOffset = (float)pbxMain.Image.Height / (float)pbxMain.Height; + Point newPoint = new Point(Convert.ToInt32((float)coordinates.X * xOffset), Convert.ToInt32((float)coordinates.Y * yOffset)); + + //MessageBox.Show("Coordinates" + Environment.NewLine + "Old : " + coordinates.ToString() + Environment.NewLine + "New : " + newPoint.ToString()); + + if (ZoneP1 == new Point(-1, -1)) + { + ZoneP1 = newPoint; + } + else + { + ZoneP2 = newPoint; + CreateNewZone(ZoneP1, ZoneP2); + SwitchZoneCreation(); + } + RefreshUI(); + } + } + private void pbxMain_Click(object sender, EventArgs e) + { + //Not the right one to use visibly + } + private void pbxDriverZone_MouseClick(object sender, MouseEventArgs e) + { + if (CreatingWindow && pbxDriverZone.Image != null) + { + Point coordinates = e.Location; + + float xOffset = (float)pbxDriverZone.Image.Width / (float)pbxDriverZone.Width; + float yOffset = (float)pbxDriverZone.Image.Height / (float)pbxDriverZone.Height; + + Point newPoint = new Point(Convert.ToInt32((float)coordinates.X * xOffset), Convert.ToInt32((float)coordinates.Y * yOffset)); + + if (WindowP1 == new Point(-1, -1)) + { + WindowP1 = newPoint; + } + else + { + WindowP2 = newPoint; + WindowsToAdd.Add(CreateAbsoluteRectangle(WindowP1, WindowP2)); + + if (WindowsToAdd.Count < ConfigurationTool.NUMBER_OF_ZONES) + { + WindowP1 = new Point(-1, -1); + WindowP2 = new Point(-1, -1); + } + else + { + WindowP1 = new Point(WindowP1.X, 0); + WindowP2 = new Point(WindowP2.X, pbxDriverZone.Image.Height); + CreateWindows(WindowsToAdd); + SwitchWindowCreation(); + } + } + RefreshUI(); + } + } + private void pbxDriverZone_Click(object sender, EventArgs e) + { + //Not the right one to use visibly + } + private Rectangle CreateAbsoluteRectangle(Point p1, Point p2) + { + Point newP1 = new Point(); + Point newP2 = new Point(); + + if (p1.X < p2.X) + { + newP1.X = p1.X; + newP2.X = p2.X; + } + else + { + newP1.X = p2.X; + newP2.X = p1.X; + } + + if (p1.Y < p2.Y) + { + newP1.Y = p1.Y; + newP2.Y = p2.Y; + } + else + { + newP1.Y = p2.Y; + newP2.Y = p1.Y; + } + return new Rectangle(newP1.X, newP1.Y, newP2.X - newP1.X, newP2.Y - newP1.Y); + } + + private async void btnRefresh_Click(object sender, EventArgs e) + { + btnRefresh.Enabled = false; + if (Emulator == null || Emulator.GrandPrixUrl != tbxGpUrl.Text) + { + Emulator = new F1TVEmulator(tbxGpUrl.Text); + } + + if (!Emulator.Ready) + { + Task start = Task.Run(() => Emulator.Start()); + int errorCode = await start; + if (errorCode != 0) + { + string message; + switch (errorCode) + { + case 101: + message = "Error " + errorCode + " Could not start the driver. It could be because an other instance is runnin make sure you closed them all before trying again"; + break; + case 102: + message = "Error " + errorCode + " Could not navigate on the F1TV site. Make sure the correct URL has been given and that you logged from chrome. It can take a few minutes to update"; + break; + case 103: + message = "Error " + errorCode + " The url is not a valid url"; + break; + case 104: + message = "Error " + errorCode + " The url is not a valid url"; + break; + case 105: + message = "Error " + errorCode + " There has been an error trying to emulate button presses. Please try again"; + break; + case 106: + message = "Error " + errorCode + " There has been an error trying to emulate button presses. Please try again"; + break; + default: + message = "Could not start the emulator Error " + errorCode; + break; + } + MessageBox.Show(message); + } + else + { + pbxMain.Image = Emulator.Screenshot(); + } + } + else + { + pbxMain.Image = Emulator.Screenshot(); + } + btnRefresh.Enabled = true; + } + + private void Settings_FormClosing(object sender, FormClosingEventArgs e) + { + if (Emulator != null) + { + Emulator.Stop(); + } + } + + private void btnResetDriver_Click(object sender, EventArgs e) + { + if (Emulator != null) + { + Emulator.ResetDriver(); + } + } + + private void btnSavePreset_Click(object sender, EventArgs e) + { + string presetName = tbxPresetName.Text; + if (Config != null) + { + Config.SaveToJson(DriverList,presetName); + } + RefreshUI(); + } + + private void lsbPresets_SelectedIndexChanged(object sender, EventArgs e) + { + //Nothing + } + + private void btnLoadPreset_Click(object sender, EventArgs e) + { + if (lsbPresets.SelectedIndex >= 0 && pbxMain.Image != null) + { + try + { + Reader reader = new Reader(lsbPresets.Items[lsbPresets.SelectedIndex].ToString(), (Bitmap)pbxMain.Image,false); + //MainZones #0 is the big main zone containing driver zones + Config = new ConfigurationTool((Bitmap)pbxMain.Image, reader.MainZones[0].Bounds); + Config.MainZone = reader.MainZones[0]; + DriverList = reader.Drivers; + } + catch (Exception ex) + { + MessageBox.Show("Could not load the settings error :" + ex); + } + RefreshUI(); + } + } + } +} + +``` diff --git a/temp_annexes/Code/Window.md b/temp_annexes/Code/Window.md new file mode 100644 index 0000000..f453494 --- /dev/null +++ b/temp_annexes/Code/Window.md @@ -0,0 +1,322 @@ +# Window.cs + +``` cs +/// Author : Maxime Rohmer +/// Date : 08/05/2023 +/// File : Window.cs +/// Brief : Default Window object that is mainly expected to be inherited. +/// Version : 0.1 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Drawing; +using System.IO; +using Tesseract; +using System.Text.RegularExpressions; +using System.Drawing.Drawing2D; + +namespace Test_Merge +{ + public class Window + { + private Rectangle _bounds; + private Bitmap _image; + private string _name; + protected TesseractEngine Engine; + public Rectangle Bounds { get => _bounds; private set => _bounds = value; } + public Bitmap Image { get => _image; set => _image = value; } + public string Name { get => _name; protected set => _name = value; } + //This will have to be changed if you want to make it run on your machine + public static DirectoryInfo TESS_DATA_FOLDER = new DirectoryInfo(@"C:\Users\Moi\Pictures\SeleniumScreens\TessData"); + + public Bitmap WindowImage + { + get + { + //This little trickery lets you have the image that the window sees + Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); + Graphics g = Graphics.FromImage(sample); + g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); + return sample; + } + } + public Window(Bitmap image, Rectangle bounds, bool generateEngine = true) + { + Image = image; + Bounds = bounds; + if (generateEngine) + { + Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, "eng", EngineMode.Default); + Engine.DefaultPageSegMode = PageSegMode.SingleLine; + } + } + /// + /// Method that will have to be used by the childrens to let the model make them decode the images they have + /// + /// Returns an object because we dont know what kind of return it will be + public virtual async Task DecodePng() + { + return "NaN"; + } + /// + /// Method that will have to be used by the childrens to let the model make them decode the images they have + /// + /// This is a list of the different possible drivers in the race. It should not be too big but NEVER be too short + /// Returns an object because we dont know what kind of return it will be + public virtual async Task DecodePng(List driverList) + { + return "NaN"; + } + /// + /// This converts an image into a byte[]. It can be usefull when doing unsafe stuff. Use at your own risks + /// + /// The image you want to convert + /// A byte array containing the image informations + public static byte[] ImageToByte(Image inputImage) + { + using (var stream = new MemoryStream()) + { + inputImage.Save(stream, System.Drawing.Imaging.ImageFormat.Png); + return stream.ToArray(); + } + } + /// + /// This method is used to recover a time from a PNG using Tesseract OCR + /// + /// The image where the text is + /// The type of window it is + /// The Tesseract Engine + /// The time in milliseconds + public static async Task GetTimeFromPng(Bitmap windowImage, OcrImage.WindowType windowType, TesseractEngine Engine) + { + //Kind of a big method but it has a lot of error handling and has to work with three special cases + string rawResult = ""; + int result = 0; + + switch (windowType) + { + case OcrImage.WindowType.Sector: + //The usual sector is in this form : 33.456 + Engine.SetVariable("tessedit_char_whitelist", "0123456789."); + break; + case OcrImage.WindowType.LapTime: + //The usual Lap time is in this form : 1:45:345 + Engine.SetVariable("tessedit_char_whitelist", "0123456789.:"); + break; + case OcrImage.WindowType.Gap: + //The usual Gap is in this form : + 34.567 + Engine.SetVariable("tessedit_char_whitelist", "0123456789.+"); + break; + default: + Engine.SetVariable("tessedit_char_whitelist", ""); + break; + } + + + Bitmap enhancedImage = new OcrImage(windowImage).Enhance(windowType); + + var tessImage = Pix.LoadFromMemory(ImageToByte(enhancedImage)); + + Page page = Engine.Process(tessImage); + Graphics g = Graphics.FromImage(enhancedImage); + // Get the iterator for the page layout + using (var iter = page.GetIterator()) + { + // Loop over the elements of the page layout + iter.Begin(); + do + { + // Get the text for the current element + try + { + rawResult += iter.GetText(PageIteratorLevel.Word); + } + catch + { + //nothing we just dont add it if its not a number + } + } while (iter.Next(PageIteratorLevel.Word)); + } + + List rawNumbers; + + //In the gaps we can find '+' but we dont care about it its redondant a driver will never be - something + if (windowType == OcrImage.WindowType.Gap) + rawResult = Regex.Replace(rawResult, "[^0-9.:]", ""); + + //Splits into minuts seconds miliseconds + rawNumbers = rawResult.Split('.', ':').ToList(); + //removes any empty cells (tho this usually sign of a really bad OCR implementation tbh will have to be fixed higher in the chian) + rawNumbers.RemoveAll(x => ((string)x) == ""); + + if (rawNumbers.Count == 3) + { + //mm:ss:ms + result = (Convert.ToInt32(rawNumbers[0]) * 1000 * 60) + (Convert.ToInt32(rawNumbers[1]) * 1000) + Convert.ToInt32(rawNumbers[2]); + } + else + { + if (rawNumbers.Count == 2) + { + //ss:ms + result = (Convert.ToInt32(rawNumbers[0]) * 1000) + Convert.ToInt32(rawNumbers[1]); + + if (result > 999999) + { + //We know that we have way too much seconds to make a minut + //Its usually because the ":" have been interpreted as a number + int minuts = (int)(rawNumbers[0][0] - '0'); + // rawNumbers[0][1] should contain the : that has been mistaken + int seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString()); + int ms = Convert.ToInt32(rawNumbers[1]); + result = (Convert.ToInt32(minuts) * 1000 * 60) + (Convert.ToInt32(seconds) * 1000) + Convert.ToInt32(ms); + } + } + else + { + if (rawNumbers.Count == 1) + { + try + { + result = Convert.ToInt32(rawNumbers[0]); + } + catch + { + //It can be because the input is empty or because its the LEADER bracket + result = 0; + } + } + else + { + //Auuuugh + result = 0; + } + } + } + page.Dispose(); + return result; + } + /// + /// Method that recovers strings from an image using Tesseract OCR + /// + /// The image of the window that contains text + /// The Tesseract engine + /// The list of allowed chars + /// The type of window the text is on. Depending on the context the OCR will behave differently + /// the string it found + public static async Task GetStringFromPng(Bitmap WindowImage, TesseractEngine Engine, string allowedChars = "", OcrImage.WindowType windowType = OcrImage.WindowType.Text) + { + string result = ""; + + Engine.SetVariable("tessedit_char_whitelist", allowedChars); + + Bitmap rawData = WindowImage; + Bitmap enhancedImage = new OcrImage(rawData).Enhance(windowType); + + Page page = Engine.Process(enhancedImage); + using (var iter = page.GetIterator()) + { + iter.Begin(); + do + { + result += iter.GetText(PageIteratorLevel.Word); + } while (iter.Next(PageIteratorLevel.Word)); + } + page.Dispose(); + return result; + } + /// + /// Get a smaller image from a bigger one + /// + /// The big bitmap you want to get a part of + /// The dimensions of the new bitmap + /// The little bitmap + protected Bitmap GetSmallBitmapFromBigOne(Bitmap inputBitmap, Rectangle newBitmapDimensions) + { + Bitmap sample = new Bitmap(newBitmapDimensions.Width, newBitmapDimensions.Height); + Graphics g = Graphics.FromImage(sample); + g.DrawImage(inputBitmap, new Rectangle(0, 0, sample.Width, sample.Height), newBitmapDimensions, GraphicsUnit.Pixel); + return sample; + } + /// + /// Returns the closest string from a list of options + /// + /// an array of all the possibilities + /// the string you want to compare + /// The closest option + protected static string FindClosestMatch(List options, string testString) + { + var closestMatch = ""; + var closestDistance = int.MaxValue; + + foreach (var item in options) + { + var distance = LevenshteinDistance(item, testString); + if (distance < closestDistance) + { + closestMatch = item; + closestDistance = distance; + } + } + return closestMatch; + } + //This method has been generated with the help of ChatGPT + /// + /// Method that computes a score of distance between two strings + /// + /// The first string (order irrelevant) + /// The second string (order irrelevant) + /// The levenshtein distance + protected static int LevenshteinDistance(string string1, string string2) + { + if (string.IsNullOrEmpty(string1)) + { + return string.IsNullOrEmpty(string2) ? 0 : string2.Length; + } + + if (string.IsNullOrEmpty(string2)) + { + return string.IsNullOrEmpty(string1) ? 0 : string1.Length; + } + + var d = new int[string1.Length + 1, string2.Length + 1]; + for (var i = 0; i <= string1.Length; i++) + { + d[i, 0] = i; + } + + for (var j = 0; j <= string2.Length; j++) + { + d[0, j] = j; + } + + for (var i = 1; i <= string1.Length; i++) + { + for (var j = 1; j <= string2.Length; j++) + { + var cost = (string1[i - 1] == string2[j - 1]) ? 0 : 1; + d[i, j] = Math.Min(Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost); + } + } + + return d[string1.Length, string2.Length]; + } + public virtual string ToJSON() + { + string result = ""; + + result += "\"" + Name + "\"" + ":{" + Environment.NewLine; + result += "\t" + "\"x\":" + Bounds.X + "," + Environment.NewLine; + result += "\t" + "\"y\":" + Bounds.Y + "," + Environment.NewLine; + result += "\t" + "\"width\":" + Bounds.Width + Environment.NewLine; + result += "}"; + + return result; + } + } +} + +``` diff --git a/temp_annexes/Code/Zone.md b/temp_annexes/Code/Zone.md new file mode 100644 index 0000000..6df76a3 --- /dev/null +++ b/temp_annexes/Code/Zone.md @@ -0,0 +1,242 @@ +# Zone.cs + +``` cs +/// Author : Maxime Rohmer +/// Date : 08/05/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 : 0.1 + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Test_Merge +{ + public class Zone + { + private Rectangle _bounds; + private List _zones; + private List _windows; + private Bitmap _image; + private string _name; + + public Bitmap ZoneImage + { + get + { + //This little trickery lets you have the image that the zone sees + Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); + Graphics g = Graphics.FromImage(sample); + g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); + return sample; + } + } + public Bitmap Image + { + get { return _image; } + set + { + //It automatically sets the image for the contained windows and zones + _image = Image; + foreach (Window w in Windows) + { + w.Image = ZoneImage; + } + foreach (Zone z in Zones) + { + z.Image = Image; + } + } + } + + public Rectangle Bounds { get => _bounds; protected set => _bounds = value; } + public List Zones { get => _zones; protected set => _zones = value; } + public List Windows { get => _windows; protected set => _windows = value; } + public string Name { get => _name; protected set => _name = value; } + + public Zone(Bitmap image, Rectangle bounds, string name) + { + Windows = new List(); + Zones = new List(); + Name = name; + + //You cant set the image in the CTOR because the processing is impossible at first initiation + _image = image; + Bounds = bounds; + } + /// + /// Adds a zone to the list of zones + /// + /// The zone you want to add + public virtual void AddZone(Zone zone) + { + Zones.Add(zone); + } + /// + /// Add a window to the list of windows + /// + /// the window you want to add + public virtual void AddWindow(Window window) + { + Windows.Add(window); + } + /// + /// Calls all the windows to do OCR and to give back the results so we can send them to the model + /// + /// A list of all the driver in the race to help with text recognition + /// A driver data object that contains all the infos about a driver + public virtual async Task Decode(List driverList) + { + int sectorCount = 0; + DriverData result = new DriverData(); + Parallel.ForEach(Windows, async w => + { + // A switch would be prettier but I dont think its supported in this C# version + if (w is DriverNameWindow) + result.Name = (string)await (w as DriverNameWindow).DecodePng(driverList); + if (w is DriverDrsWindow) + result.DRS = (bool)await (w as DriverDrsWindow).DecodePng(); + if (w is DriverGapToLeaderWindow) + result.GapToLeader = (int)await (w as DriverGapToLeaderWindow).DecodePng(); + if (w is DriverLapTimeWindow) + result.LapTime = (int)await (w as DriverLapTimeWindow).DecodePng(); + if (w is DriverPositionWindow) + result.Position = (int)await (w as DriverPositionWindow).DecodePng(); + if (w is DriverSectorWindow) + { + sectorCount++; + if (sectorCount == 1) + result.Sector1 = (int)await (w as DriverSectorWindow).DecodePng(); + if (sectorCount == 2) + result.Sector2 = (int)await (w as DriverSectorWindow).DecodePng(); + if (sectorCount == 3) + result.Sector3 = (int)await (w as DriverSectorWindow).DecodePng(); + } + if (w is DriverTyresWindow) + result.CurrentTyre = (Tyre)await (w as DriverTyresWindow).DecodePng(); + }); + return result; + } + public virtual Bitmap Draw() + { + Bitmap img; + + //If its the main zone we want to see everything + if (Zones.Count > 0) + { + img = Image; + } + else + { + img = ZoneImage; + } + + Graphics g = Graphics.FromImage(img); + + //If its the main zone we need to visualize the Zone bounds displayed + if (Zones.Count > 0) + g.DrawRectangle(new Pen(Brushes.Violet, 5), Bounds); + + foreach (Zone z in Zones) + { + Rectangle newBounds = new Rectangle(z.Bounds.X, z.Bounds.Y + Bounds.Y, z.Bounds.Width, z.Bounds.Height); + g.DrawRectangle(Pens.Red, newBounds); + } + foreach (Window w in Windows) + { + g.DrawRectangle(Pens.Blue, w.Bounds); + } + return img; + } + public void ResetZones() + { + Zones.Clear(); + } + public void ResetWindows() + { + foreach (Zone z in Zones) + { + z.ResetWindows(); + } + Windows.Clear(); + } + public virtual string ToJSON() + { + string result = ""; + result += "\"" + Name + "\":{" + Environment.NewLine; + result += "\t" + "\"x\":" + Bounds.X + "," + Environment.NewLine; + result += "\t" + "\"y\":" + Bounds.Y + "," + Environment.NewLine; + result += "\t" + "\"width\":" + Bounds.Width + "," + Environment.NewLine; + result += "\t" + "\"height\":" + Bounds.Height; + + if (Windows.Count != 0) + { + result += "," + Environment.NewLine; + + result += "\t" + "\"Windows\":[" + Environment.NewLine; + result += "\t\t{" + Environment.NewLine; + int Wcount = 0; + foreach (Window w in Windows) + { + result += "\t\t" + w.ToJSON(); + Wcount++; + if (Wcount != Windows.Count) + result += ","; + } + result += "\t\t}" + Environment.NewLine; + result += "\t" + "]" + Environment.NewLine; + } + else + { + result += Environment.NewLine; + } + if (Zones.Count != 0) + { + result += "," + Environment.NewLine; + + result += "\t" + "\"Zones\":[" + Environment.NewLine; + result += "\t\t{" + Environment.NewLine; + int Zcount = 0; + //foreach (Zone z in Zones) + //{ + result += "\t\t" + Zones[0].ToJSON(); + Zcount++; + if (Zcount != Zones.Count) + //result += ","; + //} + result += "\t\t}" + Environment.NewLine; + result += "\t" + "]" + Environment.NewLine; + } + else + { + result += Environment.NewLine; + } + + result += "}"; + + return result; + } + /// + /// Checks if the given Rectangle fits in the current zone + /// + /// The Rectangle you want to check the fittment + /// + protected bool Fits(Rectangle inputRectangle) + { + if (inputRectangle.X + inputRectangle.Width > Bounds.Width || inputRectangle.Y + inputRectangle.Height > Bounds.Height || inputRectangle.X < 0 || inputRectangle.Y < 0) + { + return false; + } + else + { + return true; + } + } + } +} + +``` diff --git a/temp_annexes/Code/recoverCookiesCSV.md b/temp_annexes/Code/recoverCookiesCSV.md new file mode 100644 index 0000000..33341f6 --- /dev/null +++ b/temp_annexes/Code/recoverCookiesCSV.md @@ -0,0 +1,88 @@ +# recoverCookiesCSV.py + +``` py +# Rohmer Maxime +# RecoverCookies.py +# Little script that recovers the cookies stored in the chrome sqlite database and then decrypts them using the key stored in the chrome files +# This script has been created to be used by an other programm or for the data to not be used directly. This is why it stores all the decoded cookies in a csv. (Btw could be smart for the end programm to delete the csv after using it) +# Parts of this cript have been created with the help of ChatGPT + +import os +import json +import base64 +import sqlite3 +import win32crypt +from Cryptodome.Cipher import AES +from pathlib import Path +import csv + +def get_master_key(): + with open( + os.getenv("localappdata") + "\\Google\\Chrome\\User Data\\Local State", "r" + ) as f: + local_state = f.read() + local_state = json.loads(local_state) + master_key = base64.b64decode(local_state["os_crypt"]["encrypted_key"]) + master_key = master_key[5:] # removing DPAPI + master_key = win32crypt.CryptUnprotectData(master_key, None, None, None, 0)[1] + print("MASTER KEY :") + print(master_key) + print(len(master_key)) + return master_key + +def decrypt_payload(cipher, payload): + return cipher.decrypt(payload) + +def generate_cipher(aes_key, iv): + return AES.new(aes_key, AES.MODE_GCM, iv) + +def decrypt_password(buff, master_key): + try: + iv = buff[3:15] + payload = buff[15:] + cipher = generate_cipher(master_key, iv) + decrypted_pass = decrypt_payload(cipher, payload) + decrypted_pass = decrypted_pass[:-16].decode() # remove suffix bytes + return decrypted_pass + except Exception: + # print("Probably saved password from Chrome version older than v80\n") + # print(str(e)) + return "Chrome < 80" + + +master_key = get_master_key() + +cookies_path = Path( + os.getenv("localappdata") + "\\Google\\Chrome\\User Data\\Default\\Network\\Cookies" +) + +if not cookies_path.exists(): + raise ValueError("Cookies file not found") + +with sqlite3.connect(cookies_path) as connection: + connection.row_factory = sqlite3.Row + cursor = connection.cursor() + cursor.execute("SELECT * FROM cookies") + + with open('cookies.csv', 'a', newline='') as csvfile: + fieldnames = ['host_key', 'name', 'value', 'path', 'expires_utc', 'is_secure', 'is_httponly'] + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + + if csvfile.tell() == 0: + writer.writeheader() + + for row in cursor.fetchall(): + decrypted_value = decrypt_password(row["encrypted_value"], master_key) + writer.writerow({ + 'host_key': row["host_key"], + 'name': row["name"], + 'value': decrypted_value, + 'path': row["path"], + 'expires_utc': row["expires_utc"], + 'is_secure': row["is_secure"], + 'is_httponly': row["is_httponly"] + }) + +print("Finished CSV") + +```