HoloLens (1. generace) a Azure 310: Detekce objektů
Poznámka
Kurzy Mixed Reality Academy byly navrženy s ohledem na HoloLens (1. generace) a Mixed Reality Asistivní náhlavní soupravy. Proto se domníváme, že je důležité ponechat tyto kurzy pro vývojáře, kteří stále hledají pokyny při vývoji pro tato zařízení. Tyto kurzy nebudou aktualizovány nejnovějšími sadami nástrojů nebo interakcemi používanými pro HoloLens 2. Budou zachovány, aby mohly pokračovat v práci na podporovaných zařízeních. V budoucnu bude k dispozici nová série kurzů, které předvedou vývoj pro HoloLens 2. Toto oznámení bude aktualizováno odkazem na tyto kurzy, jakmile budou zveřejněny.
V tomto kurzu se naučíte rozpoznávat vlastní vizuální obsah a jeho prostorovou pozici v rámci zadaného obrázku pomocí funkcí Azure Custom Vision rozpoznávání objektů v aplikaci hybridní reality.
Tato služba vám umožní vytrénovat model strojového učení pomocí obrázků objektů. Natrénovaný model pak použijete k rozpoznávání podobných objektů a přibližné jejich poloze v reálném světě, jak je to zajištěno zachycením kamery Microsoft HoloLens nebo kamerou připojenou k počítači pro imerzivní náhlavní soupravy (VR).
Azure Custom Vision rozpoznávání objektů je služba Microsoftu, která vývojářům umožňuje vytvářet vlastní klasifikátory obrázků. Tyto klasifikátory lze pak použít s novými obrázky k detekci objektů v rámci tohoto nového obrázku tím, že v samotném obrázku poskytnou hranice rámečku . Služba poskytuje jednoduchý a snadno použitelný online portál pro zjednodušení tohoto procesu. Další informace najdete na následujících odkazech:
Po dokončení tohoto kurzu budete mít aplikaci pro hybridní realitu, která bude umět následující:
- Uživatel bude moct upřet pohledem na objekt, který vytrénoval pomocí služby Azure Custom Vision, rozpoznávání objektů.
- Uživatel použije gesto klepnutí k zachycení obrázku, na který se dívá.
- Aplikace odešle image do služby Azure Custom Vision Service.
- Zobrazí se odpověď ze služby, která zobrazí výsledek rozpoznávání jako text s mezerou ve světě. Toho dosáhnete tak, že použijete Microsoft HoloLens Spatial Tracking jako způsob, jak porozumět pozici rozpoznaného objektu na světě, a pak pomocí značky přidružené k tomu, co se zjistí na obrázku, k poskytnutí textu popisku.
Kurz se také zabývá ručním nahráváním obrázků, vytvářením značek a trénováním služby k rozpoznávání různých objektů (v uvedeném příkladu šálek) nastavením hraničního rámečku v obrázku, který odešlete.
Důležité
Po vytvoření a použití aplikace by se vývojář měl vrátit ke službě Azure Custom Vision Service, identifikovat predikce vytvořené službou a určit, jestli byly správné nebo ne (pomocí označení všeho, co služba vynechala, a úpravou ohraničující rámečky). Službu je pak možné přetrénovat, což zvýší pravděpodobnost, že rozpozná objekty reálného světa.
V tomto kurzu se naučíte, jak získat výsledky ze služby Azure Custom Vision ( Rozpoznávání objektů) do ukázkové aplikace založené na Unity. Bude na vás, abyste tyto koncepty použili na vlastní aplikaci, kterou možná vytváříte.
Podpora zařízení
Kurz | HoloLens | Imerzivní náhlavní soupravy |
---|---|---|
MR a Azure 310: Detekce objektů | ✔️ |
Požadavky
Poznámka
Tento kurz je určený pro vývojáře, kteří mají základní zkušenosti s Unity a C#. Mějte také na paměti, že požadavky a písemné pokyny v tomto dokumentu představují to, co bylo testováno a ověřeno v době psaní tohoto dokumentu (červenec 2018). Můžete používat nejnovější software, jak je uvedeno v článku instalace nástrojů , i když by se nemělo předpokládat, že informace v tomto kurzu budou dokonale odpovídat tomu, co najdete v novějším softwaru, než je uvedeno níže.
Pro tento kurz doporučujeme následující hardware a software:
- Vývojový počítač
- Windows 10 Fall Creators Update (nebo novější) s povoleným vývojářským režimem
- Nejnovější Windows 10 SDK
- Unity 2017.4 LTS
- Visual Studio 2017
- Microsoft HoloLens s povoleným vývojářským režimem
- Přístup k internetu pro nastavení Azure a načtení služby Custom Vision Service
- Vyžaduje se série nejméně patnácti (15) obrázků) pro každý objekt, který má Custom Vision rozpoznat. Pokud chcete, můžete použít obrázky, které jsou již v tomto kurzu k dispozici, série šálků).
Než začnete
- Aby nedocházelo k problémům při sestavování tohoto projektu, důrazně doporučujeme vytvořit projekt uvedený v tomto kurzu v kořenové nebo téměř kořenové složce (dlouhé cesty ke složkám můžou způsobit problémy při sestavování).
- Nastavte a otestujte HoloLens. Pokud k tomu potřebujete podporu, navštivte článek o nastavení HoloLensu.
- Při zahájení vývoje nové aplikace HoloLens je vhodné provést kalibraci a ladění senzorů (někdy může pomoct provádět tyto úlohy pro každého uživatele).
Nápovědu k kalibraci potřebujete pomocí tohoto odkazu na článek Kalibrace HoloLensu.
Pokud potřebujete pomoc s laděním senzorů, použijte tento odkaz na článek o ladění senzorů HoloLens.
Kapitola 1 – Portál Custom Vision
Pokud chcete používat službu Azure Custom Vision Service, budete muset nakonfigurovat instanci této služby tak, aby byla dostupná pro vaši aplikaci.
Klikněte na Začínáme.
Přihlaste se k portálu Custom Vision Portal.
Pokud ještě nemáte účet Azure, budete si ho muset vytvořit. Pokud tento kurz sledujete v situaci ve třídě nebo testovacím prostředí, požádejte o pomoc s nastavením nového účtu svého instruktora nebo některého z specialistů.
Jakmile se poprvé přihlásíte, zobrazí se výzva k zobrazení panelu Podmínky služby . Kliknutím na zaškrtávací políčko odsouhlaste podmínky. Pak klikněte na Souhlasím.
Po vyjádření souhlasu s podmínkami se teď nacházíte v části Moje projekty . Klikněte na Nový projekt.
Na pravé straně se zobrazí karta s výzvou k zadání některých polí pro projekt.
Vložení názvu projektu
Vložení popisu projektu (volitelné)
Zvolte skupinu prostředků nebo vytvořte novou. Skupina prostředků poskytuje způsob, jak monitorovat, řídit přístup, zřizovat a spravovat fakturaci kolekce prostředků Azure. Doporučujeme udržovat všechny služby Azure přidružené k jednomu projektu (např. tyto kurzy) ve společné skupině prostředků.
Poznámka
Pokud si chcete přečíst další informace o skupinách prostředků Azure, přejděte na přidruženou dokumentaci.
Nastavte typy projektů jako Rozpoznávání objektů (Preview).
Jakmile budete hotovi, klikněte na Vytvořit projekt a budete přesměrováni na stránku projektu Custom Vision Service.
Kapitola 2 : Trénování Custom Vision projektu
Na portálu Custom Vision je vaším hlavním cílem vytrénovat projekt tak, aby rozpoznával konkrétní objekty v obrázcích.
Pro každý objekt, který má vaše aplikace rozpoznat, potřebujete alespoň patnáct (15) obrázků. Můžete použít obrázky, které jsou součástí tohoto kurzu (řada šálků).
Trénujte projekt Custom Vision:
Klikněte na + tlačítko vedle položky Značky.
Přidejte název značky, která se použije k přidružení obrázků. V tomto příkladu používáme k rozpoznávání obrázky kelímků, proto jsme značku pojmenovali Cup ( Cup). Po dokončení klikněte na Uložit .
Všimněte si, že vaše značka byla přidána (možná budete muset stránku znovu načíst, aby se zobrazila).
Klikněte na Přidat obrázky uprostřed stránky.
Klikněte na Procházet místní soubory a přejděte na obrázky, které chcete nahrát pro jeden objekt, minimálně patnáct (15).
Tip
K nahrání můžete vybrat několik obrázků najednou.
Jakmile vyberete všechny obrázky, se kterými chcete projekt trénovat, stiskněte Nahrát soubory . Soubory se začnou nahrávat. Jakmile budete mít potvrzení o nahrání, klikněte na Hotovo.
V tuto chvíli se obrázky nahrají, ale nebudou označené.
Pokud chcete obrázky označit, použijte myš. Když na obrázek najedete myší, zvýraznění výběru vám pomůže automaticky nakreslit výběr kolem objektu. Pokud není přesný, můžete nakreslit vlastní. Toho dosáhnete tak, že podržíte levým tlačítkem myši a přetáhnete oblast výběru tak, aby zahrnovala váš objekt.
Po výběru objektu v obrázku se zobrazí malá výzva k přidání značky oblasti. Vyberte dříve vytvořenou značku (ve výše uvedeném příkladu Cup) nebo pokud přidáváte další značky, zadejte ji a klikněte na tlačítko + (plus).
Pokud chcete označit další obrázek, můžete kliknout na šipku napravo od okna nebo zavřít okno značky (kliknutím na X v pravém horním rohu okna) a potom kliknout na další obrázek. Jakmile budete mít připravený další obrázek, opakujte stejný postup. Udělejte to pro všechny obrázky, které jste nahráli, dokud nebudou všechny označené.
Poznámka
Na stejném obrázku můžete vybrat několik objektů, například na následujícím obrázku:
Jakmile je všechny označíte, kliknutím na označené tlačítko na levé straně obrazovky zobrazte označené obrázky.
Teď jste připraveni službu trénovat. Klikněte na tlačítko Trénovat a spustí se první iterace trénování.
Po sestavení uvidíte dvě tlačítka s názvem Nastavit jako výchozí a Prediktivní adresa URL. Nejprve klikněte na Nastavit jako výchozí a pak klikněte na Prediktivní adresa URL.
Poznámka
Koncový bod, který je z něj poskytnutý, je nastavený na to, která iterace byla označena jako výchozí. Proto pokud později vytvoříte novou iteraci a aktualizujete ji jako výchozí, nebudete muset kód měnit.
Po kliknutí na adresu URL predikce otevřete Poznámkový blok a zkopírujte a vložte adresu URL (označovanou také jako prediktivní koncový bod) a klíč služby Prediction-Key, abyste je mohli později v kódu načíst, až ji budete potřebovat.
Kapitola 3 – Nastavení projektu Unity
Následující příklad je typickým nastavením pro vývoj s hybridní realitou a jako takový je dobrou šablonou pro další projekty.
Otevřete Unity a klikněte na Nový.
Teď budete muset zadat název projektu Unity. Vložte CustomVisionObjDetection. Ujistěte se, že je typ projektu nastavený na 3D, a nastavte umístění na místo, které je pro vás vhodné (nezapomeňte, že lepší je blíž ke kořenovým adresářům). Pak klikněte na Vytvořit projekt.
Když je Unity otevřená, stojí za to zkontrolovat, jestli je výchozí Editor skriptů nastavený na Visual Studio. Přejděte na Upravit>předvolby a pak v novém okně přejděte na Externí nástroje. Změňte Editor externích skriptů na Visual Studio. Zavřete okno Předvolby .
Pak přejděte na Nastavení sestavení souboru>, přepněte platformu na Univerzální platforma Windows a potom klikněte na tlačítko Přepnout platformu.
Ve stejném okně Nastavení sestavení se ujistěte, že jsou nastavené následující možnosti:
Cílové zařízení je nastavené na HoloLens.
Typ sestavení je nastavený na D3D.
Sada SDK je nastavená na nejnovější nainstalovanou verzi.
Verze sady Visual Studio je nastavená na nejnovější nainstalovanou verzi.
Sestavení a spuštění je nastavené na místní počítač.
Zbývající nastavení v nastavení sestavení by prozatím měla zůstat ve výchozím nastavení.
Ve stejném okně Nastavení sestavení klikněte na tlačítko Nastavení přehrávače . Tím se otevře související panel v prostoru, kde se nachází inspektor .
Na tomto panelu je potřeba ověřit několik nastavení:
Na kartě Další nastavení :
Skriptovací verze modulu runtime by měla být experimentální (ekvivalent .NET 4.6), která aktivuje potřebu restartovat editor.
Skriptovací back-end by měl být .NET.
Úroveň kompatibility rozhraní API by měla být .NET 4.6.
Na kartě Nastavení publikování v části Možnosti zaškrtněte:
InternetClient
Webcam
SpatialPerception
Dále na panelu v nastavení XR (najdete pod nastavením publikování) zaškrtněte možnost Podpora virtuální reality a pak zkontrolujte, že je přidaná sada WINDOWS MIXED REALITY SDK.
Když se vrátíte do nastavení sestavení, projekty Unity C# už nejsou šedě: zaškrtněte políčko vedle tohoto.
Zavřete okno Nastavení sestavení .
V editoru klikněte na Upravit>grafiku nastavení> projektu.
Na panelu inspektoru bude otevřeno Nastavení grafiky . Posuňte se dolů, dokud se nezobrazí pole s názvem Vždy zahrnout shadery. Přidejte slot zvětšením proměnné Size o jednu (v tomto příkladu to bylo 8, takže jsme udělali 9). Na poslední pozici pole se zobrazí nový slot, jak je znázorněno níže:
Kliknutím na malý cílový kruh vedle slotu ve slotu otevřete seznam shaderů. Vyhledejte starší shader/transparentní/difuzní shader a poklikejte na něj.
Kapitola 4 – Import balíčku CustomVisionObjDetection Unity
Pro účely tohoto kurzu máte k dispozici balíček prostředků Unity s názvem Azure-MR-310.unitypackage.
[TIP] Všechny objekty podporované Unitym, včetně celých scén, lze zabalit do souboru .unitypackage a exportovat nebo importovat v jiných projektech. Je to nejbezpečnější a nejúčinnější způsob, jak přesouvat prostředky mezi různými projekty Unity.
Balíček Azure-MR-310, který potřebujete stáhnout, najdete tady.
S řídicím panelem Unity před vámi klikněte v nabídce v horní části obrazovky na Assets (Prostředky) a pak klikněte na Import package Custom Package (Importovat vlastní balíček).>
Pomocí nástroje pro výběr souborů vyberte balíček Azure-MR-310.unitypackage a klikněte na Otevřít. Zobrazí se vám seznam součástí tohoto prostředku. Potvrďte import kliknutím na tlačítko Importovat .
Po dokončení importu si všimnete, že složky z balíčku byly přidány do složky Assets . Tento druh struktury složek je typický pro projekt Unity.
Složka Materiály obsahuje materiál používaný kurzorem pohledu.
Složka Plugins obsahuje knihovnu DLL Newtonsoft používanou kódem k deserializaci webové odpovědi služby. Dvě (2) různé verze obsažené ve složce a podsložce jsou nezbytné k tomu, aby bylo možné knihovnu používat a vytvářet editorem Unity i sestavením UPW.
Složka Prefabs obsahuje prefaby obsažené ve scéně. Jedná se o:
- GazeCursor, kurzor použitý v aplikaci. Bude spolupracovat s předfabem SpatialMapping, aby bylo možné umístit do scény nad fyzickými objekty.
- Popisek, což je objekt uživatelského rozhraní, který se v případě potřeby používá k zobrazení značky objektu ve scéně.
- SpatialMapping, což je objekt, který aplikaci umožňuje použít vytvoření virtuální mapy pomocí prostorového sledování Microsoft HoloLens.
Složka Scény , která aktuálně obsahuje předdefinované scény pro tento kurz.
Otevřete složku Scene (Scény ) na panelu projektu a poklikáním na objDetectionScene načtěte scénu, kterou použijete pro tento kurz.
Poznámka
Není zahrnut žádný kód, napíšete ho podle tohoto kurzu.
Kapitola 5 – Vytvořte třídu CustomVisionAnalyser.
V tuto chvíli jste připraveni napsat nějaký kód. Začnete třídou CustomVisionAnalyser .
Poznámka
Volání služby Custom Vision, provedená v níže uvedeném kódu, se provádějí pomocí rozhraní REST API Custom Vision. Pomocí tohoto rozhraní zjistíte, jak implementovat a používat toto rozhraní API (užitečné pro pochopení toho, jak implementovat něco podobného na vlastní pěst). Mějte na paměti, že Microsoft nabízí sadu SDK Custom Vision, která se dá použít také k volání služby. Další informace najdete v článku Custom Vision SDK.
Tato třída zodpovídá za:
Načítá se nejnovější image zachycená jako pole bajtů.
Odeslání pole bajtů do instance služby Azure Custom Vision Service k analýze
Příjem odpovědi jako řetězce JSON
Deserializace odpovědi a předání výsledné předpovědi do Třídy SceneOrganiser , která se postará o to, jak by měla být odpověď zobrazena.
Vytvoření této třídy:
Klikněte pravým tlačítkem na složku Asset umístěnou na panelu projektu a pak klikněte na Vytvořit>složku. Zavolejte složku Skripty.
Poklikáním na nově vytvořenou složku otevřete.
Klikněte pravým tlačítkem myši do složky a pak klikněte na Vytvořit>skript jazyka C#. Pojmenujte skript CustomVisionAnalyser.
Poklikáním na nový skript CustomVisionAnalyser ho otevřete v sadě Visual Studio.
Ujistěte se, že v horní části souboru odkazujete na následující obory názvů:
using Newtonsoft.Json; using System.Collections; using System.IO; using UnityEngine; using UnityEngine.Networking;
Do třídy CustomVisionAnalyser přidejte následující proměnné:
/// <summary> /// Unique instance of this class /// </summary> public static CustomVisionAnalyser Instance; /// <summary> /// Insert your prediction key here /// </summary> private string predictionKey = "- Insert your key here -"; /// <summary> /// Insert your prediction endpoint here /// </summary> private string predictionEndpoint = "Insert your prediction endpoint here"; /// <summary> /// Bite array of the image to submit for analysis /// </summary> [HideInInspector] public byte[] imageBytes;
Poznámka
Nezapomeňte vložit klíč služby Prediction-Key do proměnné predictionKey a prediktivní koncový bod do proměnné predictionEndpoint . Zkopírovali jste je do Poznámkového bloku dříve v kapitole 2, krok 14.
Pro inicializaci proměnné Instance je teď potřeba přidat kód pro Awake( ):
/// <summary> /// Initializes this class /// </summary> private void Awake() { // Allows this instance to behave like a singleton Instance = this; }
Přidejte korutin (se statickou metodou GetImageAsByteArray(), která získá výsledky analýzy obrázku zachycené třídou ImageCapture .
Poznámka
V korutinu AnalyzeImageCapture je volání třídy SceneOrganiser , kterou teprve chcete vytvořit. Proto prozatím nechte tyto řádky okomentované.
/// <summary> /// Call the Computer Vision Service to submit the image. /// </summary> public IEnumerator AnalyseLastImageCaptured(string imagePath) { Debug.Log("Analyzing..."); WWWForm webForm = new WWWForm(); using (UnityWebRequest unityWebRequest = UnityWebRequest.Post(predictionEndpoint, webForm)) { // Gets a byte array out of the saved image imageBytes = GetImageAsByteArray(imagePath); unityWebRequest.SetRequestHeader("Content-Type", "application/octet-stream"); unityWebRequest.SetRequestHeader("Prediction-Key", predictionKey); // The upload handler will help uploading the byte array with the request unityWebRequest.uploadHandler = new UploadHandlerRaw(imageBytes); unityWebRequest.uploadHandler.contentType = "application/octet-stream"; // The download handler will help receiving the analysis from Azure unityWebRequest.downloadHandler = new DownloadHandlerBuffer(); // Send the request yield return unityWebRequest.SendWebRequest(); string jsonResponse = unityWebRequest.downloadHandler.text; Debug.Log("response: " + jsonResponse); // Create a texture. Texture size does not matter, since // LoadImage will replace with the incoming image size. //Texture2D tex = new Texture2D(1, 1); //tex.LoadImage(imageBytes); //SceneOrganiser.Instance.quadRenderer.material.SetTexture("_MainTex", tex); // The response will be in JSON format, therefore it needs to be deserialized //AnalysisRootObject analysisRootObject = new AnalysisRootObject(); //analysisRootObject = JsonConvert.DeserializeObject<AnalysisRootObject>(jsonResponse); //SceneOrganiser.Instance.FinaliseLabel(analysisRootObject); } } /// <summary> /// Returns the contents of the specified image file as a byte array. /// </summary> static byte[] GetImageAsByteArray(string imageFilePath) { FileStream fileStream = new FileStream(imageFilePath, FileMode.Open, FileAccess.Read); BinaryReader binaryReader = new BinaryReader(fileStream); return binaryReader.ReadBytes((int)fileStream.Length); }
Odstraňte metody Start() a Update(), protože se nebudou používat.
Před návratem do Unity nezapomeňte uložit změny v sadě Visual Studio.
Důležité
Jak už bylo zmíněno dříve, nedělejte si starosti s kódem, který může vypadat jako chyba, protože brzy poskytnete další třídy, které je opraví.
Kapitola 6 – Vytvoření třídy CustomVisionObjects
Třída, kterou teď vytvoříte, je Třída CustomVisionObjects .
Tento skript obsahuje řadu objektů používaných jinými třídami k serializaci a deserializaci volání Custom Vision Service.
Vytvoření této třídy:
Klikněte pravým tlačítkem do složky Scripts (Skripty ) a pak klikněte na Create C# Script (Vytvořit>skript jazyka C#). Zavolejte skript CustomVisionObjects.
Poklikáním na nový skript CustomVisionObjects ho otevřete v sadě Visual Studio.
Ujistěte se, že v horní části souboru odkazujete na následující obory názvů:
using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Networking;
Odstraňte metody Start() a Update() ve třídě CustomVisionObjects , tato třída by teď měla být prázdná.
Upozornění
Je důležité, abyste pečlivě dodržovali další instrukce. Pokud vložíte nové deklarace třídy do třídy CustomVisionObjects , zobrazí se chyby kompilace v kapitole 10, které uvádějí, že AnalysisRootObject a BoundingBox nebyly nalezeny.
Přidejte následující třídy mimotřídu CustomVisionObjects . Tyto objekty používá knihovna Newtonsoft k serializaci a deserializaci dat odpovědi:
// The objects contained in this script represent the deserialized version // of the objects used by this application /// <summary> /// Web request object for image data /// </summary> class MultipartObject : IMultipartFormSection { public string sectionName { get; set; } public byte[] sectionData { get; set; } public string fileName { get; set; } public string contentType { get; set; } } /// <summary> /// JSON of all Tags existing within the project /// contains the list of Tags /// </summary> public class Tags_RootObject { public List<TagOfProject> Tags { get; set; } public int TotalTaggedImages { get; set; } public int TotalUntaggedImages { get; set; } } public class TagOfProject { public string Id { get; set; } public string Name { get; set; } public string Description { get; set; } public int ImageCount { get; set; } } /// <summary> /// JSON of Tag to associate to an image /// Contains a list of hosting the tags, /// since multiple tags can be associated with one image /// </summary> public class Tag_RootObject { public List<Tag> Tags { get; set; } } public class Tag { public string ImageId { get; set; } public string TagId { get; set; } } /// <summary> /// JSON of images submitted /// Contains objects that host detailed information about one or more images /// </summary> public class ImageRootObject { public bool IsBatchSuccessful { get; set; } public List<SubmittedImage> Images { get; set; } } public class SubmittedImage { public string SourceUrl { get; set; } public string Status { get; set; } public ImageObject Image { get; set; } } public class ImageObject { public string Id { get; set; } public DateTime Created { get; set; } public int Width { get; set; } public int Height { get; set; } public string ImageUri { get; set; } public string ThumbnailUri { get; set; } } /// <summary> /// JSON of Service Iteration /// </summary> public class Iteration { public string Id { get; set; } public string Name { get; set; } public bool IsDefault { get; set; } public string Status { get; set; } public string Created { get; set; } public string LastModified { get; set; } public string TrainedAt { get; set; } public string ProjectId { get; set; } public bool Exportable { get; set; } public string DomainId { get; set; } } /// <summary> /// Predictions received by the Service /// after submitting an image for analysis /// Includes Bounding Box /// </summary> public class AnalysisRootObject { public string id { get; set; } public string project { get; set; } public string iteration { get; set; } public DateTime created { get; set; } public List<Prediction> predictions { get; set; } } public class BoundingBox { public double left { get; set; } public double top { get; set; } public double width { get; set; } public double height { get; set; } } public class Prediction { public double probability { get; set; } public string tagId { get; set; } public string tagName { get; set; } public BoundingBox boundingBox { get; set; } }
Před návratem do Unity nezapomeňte uložit změny v sadě Visual Studio.
Kapitola 7 – Vytvoření třídy SpatialMapping
Tato třída nastaví uchycení prostorového mapování ve scéně tak, aby bylo možné detekovat kolize mezi virtuálními objekty a skutečnými objekty.
Vytvoření této třídy:
Klikněte pravým tlačítkem do složky Scripts (Skripty ) a pak klikněte na Create C# Script (Vytvořit>skript jazyka C#). Zavolejte skript SpatialMapping.
Poklikáním na nový skript SpatialMapping ho otevřete v sadě Visual Studio.
Ujistěte se, že nad třídou SpatialMapping odkazujete na následující obory názvů:
using UnityEngine; using UnityEngine.XR.WSA;
Potom přidejte následující proměnné do třídy SpatialMapping nad metodu Start():
/// <summary> /// Allows this class to behave like a singleton /// </summary> public static SpatialMapping Instance; /// <summary> /// Used by the GazeCursor as a property with the Raycast call /// </summary> internal static int PhysicsRaycastMask; /// <summary> /// The layer to use for spatial mapping collisions /// </summary> internal int physicsLayer = 31; /// <summary> /// Creates environment colliders to work with physics /// </summary> private SpatialMappingCollider spatialMappingCollider;
Přidejte hodnoty Awake() a Start():
/// <summary> /// Initializes this class /// </summary> private void Awake() { // Allows this instance to behave like a singleton Instance = this; } /// <summary> /// Runs at initialization right after Awake method /// </summary> void Start() { // Initialize and configure the collider spatialMappingCollider = gameObject.GetComponent<SpatialMappingCollider>(); spatialMappingCollider.surfaceParent = this.gameObject; spatialMappingCollider.freezeUpdates = false; spatialMappingCollider.layer = physicsLayer; // define the mask PhysicsRaycastMask = 1 << physicsLayer; // set the object as active one gameObject.SetActive(true); }
Odstraňte metodu Update().
Před návratem do Unity nezapomeňte uložit změny v sadě Visual Studio.
Kapitola 8 – Vytvoření třídy GazeCursor
Tato třída je zodpovědná za nastavení kurzoru na správné místo v reálném prostoru pomocí SpatialMappingCollider vytvořené v předchozí kapitole.
Vytvoření této třídy:
Klikněte pravým tlačítkem do složky Scripts (Skripty ) a pak klikněte na Create C# Script (Vytvořit>skript jazyka C#). Volání skriptu GazeCursor
Poklikáním na nový skript GazeCursor ho otevřete v sadě Visual Studio.
Ujistěte se, že nad třídou GazeCursor odkazuje následující obor názvů:
using UnityEngine;
Pak přidejte následující proměnnou do třídy GazeCursor nad metodu Start().
/// <summary> /// The cursor (this object) mesh renderer /// </summary> private MeshRenderer meshRenderer;
Aktualizujte metodu Start() následujícím kódem:
/// <summary> /// Runs at initialization right after the Awake method /// </summary> void Start() { // Grab the mesh renderer that is on the same object as this script. meshRenderer = gameObject.GetComponent<MeshRenderer>(); // Set the cursor reference SceneOrganiser.Instance.cursor = gameObject; gameObject.GetComponent<Renderer>().material.color = Color.green; // If you wish to change the size of the cursor you can do so here gameObject.transform.localScale = new Vector3(0.01f, 0.01f, 0.01f); }
Aktualizujte metodu Update() následujícím kódem:
/// <summary> /// Update is called once per frame /// </summary> void Update() { // Do a raycast into the world based on the user's head position and orientation. Vector3 headPosition = Camera.main.transform.position; Vector3 gazeDirection = Camera.main.transform.forward; RaycastHit gazeHitInfo; if (Physics.Raycast(headPosition, gazeDirection, out gazeHitInfo, 30.0f, SpatialMapping.PhysicsRaycastMask)) { // If the raycast hit a hologram, display the cursor mesh. meshRenderer.enabled = true; // Move the cursor to the point where the raycast hit. transform.position = gazeHitInfo.point; // Rotate the cursor to hug the surface of the hologram. transform.rotation = Quaternion.FromToRotation(Vector3.up, gazeHitInfo.normal); } else { // If the raycast did not hit a hologram, hide the cursor mesh. meshRenderer.enabled = false; } }
Poznámka
Nedělejte si starosti s chybou pro třídu SceneOrganiser nebyla nalezena, vytvoříte ji v další kapitole.
Před návratem do Unity nezapomeňte uložit změny v sadě Visual Studio.
Kapitola 9 – Vytvoření třídy SceneOrganiser
Tato třída:
Nastavte hlavní kameru tak, že k ní připojíte příslušné komponenty.
Když zjistíte objekt, bude zodpovědný za výpočet jeho pozice v reálném světě a umístí popisek značky poblíž s odpovídajícím názvem značky.
Vytvoření této třídy:
Klikněte pravým tlačítkem do složky Scripts (Skripty ) a pak klikněte na Create C# Script (Vytvořit>skript jazyka C#). Pojmenujte skript SceneOrganiser.
Poklikáním na nový skript SceneOrganiser ho otevřete v sadě Visual Studio.
Ujistěte se, že nad třídou SceneOrganiser odkazujete na následující obory názvů:
using System.Collections.Generic; using System.Linq; using UnityEngine;
Pak přidejte následující proměnné do třídy SceneOrganiser nad metodu Start():
/// <summary> /// Allows this class to behave like a singleton /// </summary> public static SceneOrganiser Instance; /// <summary> /// The cursor object attached to the Main Camera /// </summary> internal GameObject cursor; /// <summary> /// The label used to display the analysis on the objects in the real world /// </summary> public GameObject label; /// <summary> /// Reference to the last Label positioned /// </summary> internal Transform lastLabelPlaced; /// <summary> /// Reference to the last Label positioned /// </summary> internal TextMesh lastLabelPlacedText; /// <summary> /// Current threshold accepted for displaying the label /// Reduce this value to display the recognition more often /// </summary> internal float probabilityThreshold = 0.8f; /// <summary> /// The quad object hosting the imposed image captured /// </summary> private GameObject quad; /// <summary> /// Renderer of the quad object /// </summary> internal Renderer quadRenderer;
Odstraňte metody Start() a Update().
Pod proměnné přidejte metodu Awake(), která inicializuje třídu a nastaví scénu.
/// <summary> /// Called on initialization /// </summary> private void Awake() { // Use this class instance as singleton Instance = this; // Add the ImageCapture class to this Gameobject gameObject.AddComponent<ImageCapture>(); // Add the CustomVisionAnalyser class to this Gameobject gameObject.AddComponent<CustomVisionAnalyser>(); // Add the CustomVisionObjects class to this Gameobject gameObject.AddComponent<CustomVisionObjects>(); }
Přidejte metodu PlaceAnalysisLabel(), která vytvoří instanci popisku ve scéně (která je v tomto okamžiku pro uživatele neviditelná). Také umístí čtyřúhelník (také neviditelný) na místo, kde je obrázek umístěn, a překrývá se s reálným světem. To je důležité, protože souřadnice pole načtené ze služby po analýze jsou trasovány zpět do tohoto čtyřúhelníku, aby se určilo přibližné umístění objektu v reálném světě.
/// <summary> /// Instantiate a Label in the appropriate location relative to the Main Camera. /// </summary> public void PlaceAnalysisLabel() { lastLabelPlaced = Instantiate(label.transform, cursor.transform.position, transform.rotation); lastLabelPlacedText = lastLabelPlaced.GetComponent<TextMesh>(); lastLabelPlacedText.text = ""; lastLabelPlaced.transform.localScale = new Vector3(0.005f,0.005f,0.005f); // Create a GameObject to which the texture can be applied quad = GameObject.CreatePrimitive(PrimitiveType.Quad); quadRenderer = quad.GetComponent<Renderer>() as Renderer; Material m = new Material(Shader.Find("Legacy Shaders/Transparent/Diffuse")); quadRenderer.material = m; // Here you can set the transparency of the quad. Useful for debugging float transparency = 0f; quadRenderer.material.color = new Color(1, 1, 1, transparency); // Set the position and scale of the quad depending on user position quad.transform.parent = transform; quad.transform.rotation = transform.rotation; // The quad is positioned slightly forward in font of the user quad.transform.localPosition = new Vector3(0.0f, 0.0f, 3.0f); // The quad scale as been set with the following value following experimentation, // to allow the image on the quad to be as precisely imposed to the real world as possible quad.transform.localScale = new Vector3(3f, 1.65f, 1f); quad.transform.parent = null; }
Přidejte metodu FinaleLabel(). Zodpovídá za:
- Nastavení textu popisku pomocí značky Predikce s nejvyšší spolehlivostí
- Volání výpočtu ohraničujícího rámečku na čtyřúhelníku objektu, umístěného dříve, a umístění popisku do scény.
- Úprava hloubky popisku pomocí raycastu směrem k ohraničující rámeček, který by měl kolidovat s objektem v reálném světě.
- Resetování procesu zachycení, aby uživatel mohl zachytit jinou image.
/// <summary> /// Set the Tags as Text of the last label created. /// </summary> public void FinaliseLabel(AnalysisRootObject analysisObject) { if (analysisObject.predictions != null) { lastLabelPlacedText = lastLabelPlaced.GetComponent<TextMesh>(); // Sort the predictions to locate the highest one List<Prediction> sortedPredictions = new List<Prediction>(); sortedPredictions = analysisObject.predictions.OrderBy(p => p.probability).ToList(); Prediction bestPrediction = new Prediction(); bestPrediction = sortedPredictions[sortedPredictions.Count - 1]; if (bestPrediction.probability > probabilityThreshold) { quadRenderer = quad.GetComponent<Renderer>() as Renderer; Bounds quadBounds = quadRenderer.bounds; // Position the label as close as possible to the Bounding Box of the prediction // At this point it will not consider depth lastLabelPlaced.transform.parent = quad.transform; lastLabelPlaced.transform.localPosition = CalculateBoundingBoxPosition(quadBounds, bestPrediction.boundingBox); // Set the tag text lastLabelPlacedText.text = bestPrediction.tagName; // Cast a ray from the user's head to the currently placed label, it should hit the object detected by the Service. // At that point it will reposition the label where the ray HL sensor collides with the object, // (using the HL spatial tracking) Debug.Log("Repositioning Label"); Vector3 headPosition = Camera.main.transform.position; RaycastHit objHitInfo; Vector3 objDirection = lastLabelPlaced.position; if (Physics.Raycast(headPosition, objDirection, out objHitInfo, 30.0f, SpatialMapping.PhysicsRaycastMask)) { lastLabelPlaced.position = objHitInfo.point; } } } // Reset the color of the cursor cursor.GetComponent<Renderer>().material.color = Color.green; // Stop the analysis process ImageCapture.Instance.ResetImageCapture(); }
Přidejte metodu CalculateBoundingBoxPosition(), která hostuje řadu výpočtů potřebných k překladu souřadnic ohraničujícího rámečku načtených ze služby a jejich proporcionálnímu vytvoření na čtyřúhelníku.
/// <summary> /// This method hosts a series of calculations to determine the position /// of the Bounding Box on the quad created in the real world /// by using the Bounding Box received back alongside the Best Prediction /// </summary> public Vector3 CalculateBoundingBoxPosition(Bounds b, BoundingBox boundingBox) { Debug.Log($"BB: left {boundingBox.left}, top {boundingBox.top}, width {boundingBox.width}, height {boundingBox.height}"); double centerFromLeft = boundingBox.left + (boundingBox.width / 2); double centerFromTop = boundingBox.top + (boundingBox.height / 2); Debug.Log($"BB CenterFromLeft {centerFromLeft}, CenterFromTop {centerFromTop}"); double quadWidth = b.size.normalized.x; double quadHeight = b.size.normalized.y; Debug.Log($"Quad Width {b.size.normalized.x}, Quad Height {b.size.normalized.y}"); double normalisedPos_X = (quadWidth * centerFromLeft) - (quadWidth/2); double normalisedPos_Y = (quadHeight * centerFromTop) - (quadHeight/2); return new Vector3((float)normalisedPos_X, (float)normalisedPos_Y, 0); }
Před návratem do Unity nezapomeňte uložit změny v sadě Visual Studio.
Důležité
Než budete pokračovat, otevřete třídu CustomVisionAnalyser a v metodě AnalyzeLastImageCaptured()zrušte komentář následujících řádků:
// Create a texture. Texture size does not matter, since // LoadImage will replace with the incoming image size. Texture2D tex = new Texture2D(1, 1); tex.LoadImage(imageBytes); SceneOrganiser.Instance.quadRenderer.material.SetTexture("_MainTex", tex); // The response will be in JSON format, therefore it needs to be deserialized AnalysisRootObject analysisRootObject = new AnalysisRootObject(); analysisRootObject = JsonConvert.DeserializeObject<AnalysisRootObject>(jsonResponse); SceneOrganiser.Instance.FinaliseLabel(analysisRootObject);
Poznámka
Nedělejte si starosti se zprávou Třídy ImageCapture "nepodařilo se najít", vytvoříte ji v další kapitole.
Kapitola 10 – Vytvoření třídy ImageCapture
Další třídou, kterou vytvoříte, je třída ImageCapture .
Tato třída zodpovídá za:
- Zachycení obrázku pomocí kamery HoloLens a jeho uložení do složky Aplikace
- Zpracování gest klepnutí od uživatele.
Vytvoření této třídy:
Přejděte do složky Scripts , kterou jste vytvořili dříve.
Klikněte pravým tlačítkem myši do složky a pak klikněte na Vytvořit>skript jazyka C#. Pojmenujte skript ImageCapture.
Poklikáním na nový skript ImageCapture ho otevřete v sadě Visual Studio.
Obory názvů v horní části souboru nahraďte následujícím kódem:
using System; using System.IO; using System.Linq; using UnityEngine; using UnityEngine.XR.WSA.Input; using UnityEngine.XR.WSA.WebCam;
Pak přidejte následující proměnné do třídy ImageCapture nad metodu Start():
/// <summary> /// Allows this class to behave like a singleton /// </summary> public static ImageCapture Instance; /// <summary> /// Keep counts of the taps for image renaming /// </summary> private int captureCount = 0; /// <summary> /// Photo Capture object /// </summary> private PhotoCapture photoCaptureObject = null; /// <summary> /// Allows gestures recognition in HoloLens /// </summary> private GestureRecognizer recognizer; /// <summary> /// Flagging if the capture loop is running /// </summary> internal bool captureIsActive; /// <summary> /// File path of current analysed photo /// </summary> internal string filePath = string.Empty;
Teď je potřeba přidat kód pro metody Awake() a Start( ):
/// <summary> /// Called on initialization /// </summary> private void Awake() { Instance = this; } /// <summary> /// Runs at initialization right after Awake method /// </summary> void Start() { // Clean up the LocalState folder of this application from all photos stored DirectoryInfo info = new DirectoryInfo(Application.persistentDataPath); var fileInfo = info.GetFiles(); foreach (var file in fileInfo) { try { file.Delete(); } catch (Exception) { Debug.LogFormat("Cannot delete file: ", file.Name); } } // Subscribing to the Microsoft HoloLens API gesture recognizer to track user gestures recognizer = new GestureRecognizer(); recognizer.SetRecognizableGestures(GestureSettings.Tap); recognizer.Tapped += TapHandler; recognizer.StartCapturingGestures(); }
Implementujte obslužnou rutinu, která se bude volat, když dojde k gestu klepnutí:
/// <summary> /// Respond to Tap Input. /// </summary> private void TapHandler(TappedEventArgs obj) { if (!captureIsActive) { captureIsActive = true; // Set the cursor color to red SceneOrganiser.Instance.cursor.GetComponent<Renderer>().material.color = Color.red; // Begin the capture loop Invoke("ExecuteImageCaptureAndAnalysis", 0); } }
Důležité
Když je kurzor zelený, znamená to, že kamera je k dispozici pro pořízení snímku. Když je kurzor červený, znamená to, že kamera je zaneprázdněná.
Přidejte metodu, kterou aplikace použije ke spuštění procesu zachycení image a uložení image:
/// <summary> /// Begin process of image capturing and send to Azure Custom Vision Service. /// </summary> private void ExecuteImageCaptureAndAnalysis() { // Create a label in world space using the ResultsLabel class // Invisible at this point but correctly positioned where the image was taken SceneOrganiser.Instance.PlaceAnalysisLabel(); // Set the camera resolution to be the highest possible Resolution cameraResolution = PhotoCapture.SupportedResolutions.OrderByDescending ((res) => res.width * res.height).First(); Texture2D targetTexture = new Texture2D(cameraResolution.width, cameraResolution.height); // Begin capture process, set the image format PhotoCapture.CreateAsync(true, delegate (PhotoCapture captureObject) { photoCaptureObject = captureObject; CameraParameters camParameters = new CameraParameters { hologramOpacity = 1.0f, cameraResolutionWidth = targetTexture.width, cameraResolutionHeight = targetTexture.height, pixelFormat = CapturePixelFormat.BGRA32 }; // Capture the image from the camera and save it in the App internal folder captureObject.StartPhotoModeAsync(camParameters, delegate (PhotoCapture.PhotoCaptureResult result) { string filename = string.Format(@"CapturedImage{0}.jpg", captureCount); filePath = Path.Combine(Application.persistentDataPath, filename); captureCount++; photoCaptureObject.TakePhotoAsync(filePath, PhotoCaptureFileOutputFormat.JPG, OnCapturedPhotoToDisk); }); }); }
Přidejte obslužné rutiny, které se budou volat, jakmile bude fotka zachycena a kdy je připravená k analýze. Výsledek se pak předá do CustomVisionAnalyser k analýze.
/// <summary> /// Register the full execution of the Photo Capture. /// </summary> void OnCapturedPhotoToDisk(PhotoCapture.PhotoCaptureResult result) { try { // Call StopPhotoMode once the image has successfully captured photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode); } catch (Exception e) { Debug.LogFormat("Exception capturing photo to disk: {0}", e.Message); } } /// <summary> /// The camera photo mode has stopped after the capture. /// Begin the image analysis process. /// </summary> void OnStoppedPhotoMode(PhotoCapture.PhotoCaptureResult result) { Debug.LogFormat("Stopped Photo Mode"); // Dispose from the object in memory and request the image analysis photoCaptureObject.Dispose(); photoCaptureObject = null; // Call the image analysis StartCoroutine(CustomVisionAnalyser.Instance.AnalyseLastImageCaptured(filePath)); } /// <summary> /// Stops all capture pending actions /// </summary> internal void ResetImageCapture() { captureIsActive = false; // Set the cursor color to green SceneOrganiser.Instance.cursor.GetComponent<Renderer>().material.color = Color.green; // Stop the capture loop if active CancelInvoke(); }
Před návratem do Unity nezapomeňte uložit změny v sadě Visual Studio.
Kapitola 11 – Nastavení skriptů ve scéně
Teď, když jste napsali veškerý kód potřebný pro tento projekt, je čas nastavit skripty ve scéně a na předfabách, aby se chovaly správně.
V Editoru Unity na panelu hierarchie vyberte hlavní kameru.
Na panelu inspektoru s vybranou hlavní kamerou klikněte na Přidat komponentu, vyhledejte skript SceneOrganiser a poklikáním ho přidejte.
Na panelu projektu otevřete složku Prefabs a přetáhněte prefabLabel do vstupní oblasti Popisek prázdného cílového odkazu ve skriptu SceneOrganiser , který jste právě přidali do hlavní kamery, jak je znázorněno na následujícím obrázku:
Na panelu hierarchie vyberte podřízený objekt GazeCursorhlavní kamery.
Na panelu inspektoru s vybranou možností GazeCursor klikněte na Přidat komponentu, vyhledejte skript GazeCursor a poklikáním ho přidejte.
Znovu na panelu hierarchie vyberte podřízenou položku SpatialMappinghlavní kamery.
Na panelu inspektoru s vybranou možností SpatialMapping klikněte na Přidat komponentu, vyhledejte skript SpatialMapping a poklikáním ho přidejte.
Zbývající skripty, které jste nenastavili, budou přidány kódem ve skriptu SceneOrganiser za běhu.
Kapitola 12 - Před budovou
Pokud chcete provést důkladný test aplikace, budete ji muset bokem načíst do Microsoft HoloLens.
Než to uděláte, ujistěte se, že:
Všechna nastavení uvedená v kapitole 3 jsou nastavena správně.
Skript SceneOrganiser je připojen k objektu Main Camera .
Skript GazeCursor je připojen k objektu GazeCursor .
Skript SpatialMapping je připojen k objektu SpatialMapping .
V kapitole 5 krok 6:
- Nezapomeňte vložit klíč predikce služby do proměnné predictionKey .
- Do třídy predictionEndpoint jste vložili koncový bod předpovědi.
Kapitola 13 – Sestavení řešení PRO UPW a zkušební načtení aplikace
Teď jste připraveni sestavit aplikaci jako řešení PRO UPW, které budete moct nasadit do Microsoft HoloLens. Zahájení procesu sestavení:
Přejděte na Nastavení sestavení souboru>.
Zaškrtněte políčko Projekty Unity C#.
Klikněte na Přidat otevřené scény. Tím se do sestavení přidá aktuálně otevřená scéna.
Klikněte na Sestavit. Unity spustí Průzkumník souborů okno, ve kterém musíte vytvořit a pak vybrat složku, do které chcete aplikaci sestavit. Vytvořte teď složku a pojmenujte ji App. Potom vyberte složku Aplikace a klikněte na Vybrat složku.
Unity začne vytvářet projekt do složky Aplikace .
Jakmile Unity dokončí sestavování (může to nějakou dobu trvat), otevře se okno Průzkumník souborů v umístění vašeho buildu (zkontrolujte hlavní panel, protože se nemusí vždy zobrazovat nad vašimi okny, ale upozorní vás na přidání nového okna).
Pokud chcete provést nasazení do Microsoft HoloLens, budete potřebovat IP adresu tohoto zařízení (pro vzdálené nasazení) a zajistit, že má také nastavený režim vývojáře. Použijte následující postup:
Při nošení HoloLensu otevřete Nastavení.
Přejděte do částiRozšířené možnostiwi-fi>sítě & internetu>.
Poznamenejte si IPv4 adresu.
Pak přejděte zpět na Nastavení a pak na Aktualizovat zabezpečení &>pro vývojáře.
Nastavte vývojářský režimna zapnuto.
Přejděte do nového sestavení Unity (složka Aplikace ) a otevřete soubor řešení v sadě Visual Studio.
V části Konfigurace řešení vyberte Ladit.
Na platformě řešení vyberte x86, Vzdálený počítač. Zobrazí se výzva k vložení IP adresy vzdáleného zařízení (v tomto případě Microsoft HoloLens, kterou jste si poznamenali).
Přejděte do nabídky Sestavení a klikněte na Nasadit řešení a načtěte aplikaci do HoloLensu bokem.
Vaše aplikace by se teď měla zobrazit v seznamu nainstalovaných aplikací na vašem Microsoft HoloLens a měla by být připravená ke spuštění.
Použití aplikace:
- Podívejte se na objekt, který jste vytrénovali pomocí služby Azure Custom Vision a rozpoznávání objektů, a použijte gesto klepnutí.
- Pokud se objekt úspěšně rozpozná, zobrazí se světoprostorový text popisku s názvem značky.
Důležité
Pokaždé, když pořídíte fotku a odešlete ji službě, můžete se vrátit na stránku Služba a znovu natrénovat službu s nově zachycenými obrázky. Na začátku budete pravděpodobně také muset opravit ohraničující rámečky , aby byly přesnější a přetrénovat službu.
Poznámka
Umístěný text popisku se nemusí zobrazit v blízkosti objektu, pokud Microsoft HoloLens senzory a/nebo SpatialTrackingComponent v Unity nedokáže umístit příslušné kolidéry vzhledem k objektům reálného světa. Pokud tomu tak je, zkuste aplikaci použít na jiném povrchu.
Vaše aplikace Custom Vision, Rozpoznávání objektů
Blahopřejeme, vytvořili jste aplikaci hybridní reality, která využívá azure Custom Vision rozhraní API pro detekci objektů, které dokáže rozpoznat objekt z obrázku a pak poskytnout přibližnou pozici objektu v 3D prostoru.
Bonusová cvičení
Cvičení 1
Přidáním do textového popisku pomocí poloprůhledné datové krychle zabalte skutečný objekt do 3D ohraničujícího rámečku.
Cvičení 2
Trénujte službu Custom Vision Service tak, aby rozpoznala více objektů.
Cvičení 3
Přehraje zvuk, když je objekt rozpoznán.
Cvičení 4
Pomocí rozhraní API znovu vytrénujte službu se stejnými obrázky, které vaše aplikace analyzuje, aby byla služba přesnější (proveďte predikci i trénování současně).