// Lot AP7 - voir LPAnimIdentifyCommandlet.h #include "LPAnimIdentifyCommandlet.h" #include "Dom/JsonObject.h" #include "HAL/FileManager.h" #include "LPPoseMeasure.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" #include "Serialization/JsonReader.h" #include "Serialization/JsonSerializer.h" #include "Serialization/JsonWriter.h" DEFINE_LOG_CATEGORY_STATIC(LogLPAnimIdentify, Log, All); namespace LPAnimIdentifyImpl { constexpr float Minimum = 0.95f; struct FProfileScore { FString Id; int32 Wanted = 0; int32 Present = 0; TArray Missing; float Score() const { return Wanted > 0 ? static_cast(Present) / Wanted : 0.f; } }; // Meme regle que LPSkeletonProfile::Identify : tous les os cites par les roles du profil. bool ScoreProfile(const FString& Path, const TArray& Bones, FProfileScore& Out) { FString Text; TSharedPtr Root; const TSharedPtr* Roles = nullptr; if (!FFileHelper::LoadFileToString(Text, *Path) || !FJsonSerializer::Deserialize(TJsonReaderFactory<>::Create(Text), Root) || !Root.IsValid() || !Root->TryGetObjectField(TEXT("roles"), Roles)) { return false; } Out.Id = Root->GetStringField(TEXT("id")); TSet Seen; for (const TPair>& Role : (*Roles)->Values) { TArray Names; const TArray>* List = nullptr; if (Role.Value->TryGetArray(List)) { for (const TSharedPtr& Item : *List) { Names.Add(Item->AsString()); } } else { Names.Add(Role.Value->AsString()); } for (const FString& Name : Names) { ++Out.Wanted; if (Bones.Contains(FName(*Name))) { ++Out.Present; } else if (!Seen.Contains(Name)) { Seen.Add(Name); Out.Missing.Add(Name); } } } return !Out.Id.IsEmpty(); } } ULPAnimIdentifyCommandlet::ULPAnimIdentifyCommandlet() { IsClient = false; IsServer = false; IsEditor = true; LogToConsole = true; } int32 ULPAnimIdentifyCommandlet::Main(const FString& Params) { using namespace LPAnimIdentifyImpl; FString Clip; FString Out; FString Expected; FParse::Value(*Params, TEXT("Clip="), Clip, false); FParse::Value(*Params, TEXT("Out="), Out, false); FParse::Value(*Params, TEXT("Attendu="), Expected, false); if (Clip.IsEmpty()) { UE_LOG(LogLPAnimIdentify, Error, TEXT("Usage : -run=LPAnimIdentify -Clip=/Game// [-Out=] [-Attendu=]")); return 1; } if (Out.IsEmpty()) { Out = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("AnimPipeline"), TEXT("identification.json")); } FLPPose Pose; if (!LPPoseMeasure::Sample(Clip, 0.f, Pose)) { UE_LOG(LogLPAnimIdentify, Error, TEXT("Clip illisible : %s"), *Clip); return 1; } const FString Dir = FPaths::Combine(FPaths::ProjectDir(), TEXT("Tools"), TEXT("anim"), TEXT("profiles")); TArray Files; IFileManager::Get().FindFiles(Files, *FPaths::Combine(Dir, TEXT("*.json")), true, false); Files.Sort(); TArray Scores; for (const FString& File : Files) { FProfileScore Score; if (ScoreProfile(FPaths::Combine(Dir, File), Pose.Names, Score)) { Scores.Add(MoveTemp(Score)); } } // Meilleur score ; a egalite, le profil qui reconnait le plus d'os (le plus specifique). const FProfileScore* Best = nullptr; for (const FProfileScore& Score : Scores) { if (!Best || Score.Score() > Best->Score() + KINDA_SMALL_NUMBER || (FMath::IsNearlyEqual(Score.Score(), Best->Score()) && Score.Present > Best->Present)) { Best = &Score; } } const bool bFound = Best && Best->Score() >= Minimum; const FString Chosen = bFound ? Best->Id : FString(); TSharedRef Report = MakeShared(); Report->SetStringField(TEXT("clip"), Clip); Report->SetNumberField(TEXT("os"), Pose.Names.Num()); Report->SetStringField(TEXT("profil"), Chosen); Report->SetStringField(TEXT("profil_mesure"), Pose.Profile); // Methode de transformation declaree pour ce profil source (Tools/anim/methodes). const FLPTransformMethod* Method = bFound ? LPPoseMeasure::MethodForProfile(Chosen) : nullptr; Report->SetStringField(TEXT("methode"), Method ? Method->Id : FString()); Report->SetStringField(TEXT("pose_calques"), Method ? (Method->bPoseT ? TEXT("T") : TEXT("A")) : FString()); Report->SetBoolField(TEXT("methode_factice"), Method && Method->bDummy); TArray> ScoreArray; for (const FProfileScore& Score : Scores) { TSharedRef Item = MakeShared(); Item->SetStringField(TEXT("id"), Score.Id); Item->SetNumberField(TEXT("score_pct"), FMath::RoundToFloat(Score.Score() * 1000.f) / 10.f); Item->SetNumberField(TEXT("os_reconnus"), Score.Present); Item->SetNumberField(TEXT("os_du_profil"), Score.Wanted); TArray> Missing; for (const FString& Name : Score.Missing) { Missing.Add(MakeShared(Name)); } Item->SetArrayField(TEXT("manquants"), Missing); ScoreArray.Add(MakeShared(Item)); UE_LOG(LogLPAnimIdentify, Display, TEXT(" %-16s %5.1f %% (%d / %d), %d manquant(s)"), *Score.Id, Score.Score() * 100.f, Score.Present, Score.Wanted, Score.Missing.Num()); } Report->SetArrayField(TEXT("profils"), ScoreArray); TArray> Bones; for (int32 Bone = 0; Bone < Pose.Names.Num(); ++Bone) { TSharedRef Item = MakeShared(); Item->SetStringField(TEXT("nom"), Pose.Names[Bone].ToString()); Item->SetStringField(TEXT("parent"), Pose.Parents.IsValidIndex(Bone) && Pose.Names.IsValidIndex(Pose.Parents[Bone]) ? Pose.Names[Pose.Parents[Bone]].ToString() : FString()); const FVector Rest = Pose.Rest.IsValidIndex(Bone) ? Pose.Rest[Bone].GetLocation() : FVector::ZeroVector; Item->SetArrayField(TEXT("repos"), { MakeShared(FMath::RoundToFloat(Rest.X * 10.f) / 10.f), MakeShared(FMath::RoundToFloat(Rest.Y * 10.f) / 10.f), MakeShared(FMath::RoundToFloat(Rest.Z * 10.f) / 10.f) }); Bones.Add(MakeShared(Item)); } Report->SetArrayField(TEXT("squelette"), Bones); TArray Failures; if (!bFound) { Failures.Add(FString::Printf(TEXT("squelette inconnu : meilleur profil %s a %.1f %% (< 95 %%)"), Best ? *Best->Id : TEXT("aucun"), Best ? Best->Score() * 100.f : 0.f)); } else if (Pose.Profile != Chosen) { Failures.Add(FString::Printf(TEXT("le module de mesure identifie %s, l'identification retient %s"), *Pose.Profile, *Chosen)); } if (bFound && !Method) { Failures.Add(FString::Printf(TEXT("aucune methode de transformation pour le profil %s (Tools/anim/methodes)"), *Chosen)); } if (!Expected.IsEmpty() && Expected != Chosen) { Failures.Add(FString::Printf(TEXT("profil attendu %s, retenu %s"), *Expected, *Chosen)); } TArray> Errors; for (const FString& Failure : Failures) { Errors.Add(MakeShared(Failure)); UE_LOG(LogLPAnimIdentify, Error, TEXT("%s"), *Failure); } Report->SetArrayField(TEXT("echecs"), Errors); Report->SetBoolField(TEXT("valide"), Failures.Num() == 0); FString Text; FJsonSerializer::Serialize(Report, TJsonWriterFactory<>::Create(&Text)); IFileManager::Get().MakeDirectory(*FPaths::GetPath(Out), true); if (!FFileHelper::SaveStringToFile(Text, *Out, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) { UE_LOG(LogLPAnimIdentify, Error, TEXT("ecriture impossible : %s"), *Out); return 1; } UE_LOG(LogLPAnimIdentify, Warning, TEXT("IDENTIFICATION %s : profil %s (mesure %s), methode %s, %d os -> %s"), *Clip, Chosen.IsEmpty() ? TEXT("aucun") : *Chosen, *Pose.Profile, Method ? *Method->Id : TEXT("aucune"), Pose.Names.Num(), *Out); return Failures.Num() > 0 ? 1 : 0; }