320 lines
10 KiB
C#
320 lines
10 KiB
C#
using System.Collections;
|
|
using UnityEngine;
|
|
using Unity.Barracuda;
|
|
using TMPro;
|
|
using UnityEngine.UI; // Untuk komponen Image
|
|
|
|
public class OrganikClassifier : MonoBehaviour
|
|
{
|
|
[Header("Model Barracuda")]
|
|
public NNModel onnxModel;
|
|
private Model runtimeModel;
|
|
private IWorker worker;
|
|
|
|
[Header("UI Text")]
|
|
public TextMeshProUGUI outputText;
|
|
public TextMeshProUGUI scoreText;
|
|
|
|
[Header("UI Image")]
|
|
public Image displayImage; // Menampilkan foto hasil kamera
|
|
|
|
[Header("Feedback Images")]
|
|
public Image feedbackCorrectImage; // Image checklist
|
|
public Image feedbackWrongImage; // Image silang
|
|
public float feedbackDelay = 1.5f; // Delay sebelum muncul canvas (dalam detik)
|
|
|
|
[Header("Canvas")]
|
|
public GameObject canvasBerhasil;
|
|
public GameObject canvasGagal;
|
|
|
|
[Header("Audio")]
|
|
[SerializeField] private AudioSource audioSource;
|
|
[SerializeField] private AudioClip correctClip;
|
|
[SerializeField] private AudioClip wrongClip;
|
|
[SerializeField] private AudioClip awardClip; // Pastikan penamaan konsisten (gunakan huruf kecil)
|
|
|
|
// Urutan kelas (sesuai training)
|
|
private string[] classes = { "anorganik", "bukansampah", "organik" };
|
|
|
|
private int inputWidth = 224;
|
|
private int inputHeight = 224;
|
|
|
|
private int currentScore = 0;
|
|
private int maxScore = 5;
|
|
|
|
void Start()
|
|
{
|
|
// Muat model dari asset
|
|
runtimeModel = ModelLoader.Load(onnxModel);
|
|
// Buat worker untuk melakukan inferensi (auto memilih antara CPU/GPU)
|
|
worker = WorkerFactory.CreateWorker(WorkerFactory.Type.Auto, runtimeModel);
|
|
|
|
// Pastikan feedback image tersembunyi awalnya
|
|
if (feedbackCorrectImage != null)
|
|
feedbackCorrectImage.gameObject.SetActive(false);
|
|
if (feedbackWrongImage != null)
|
|
feedbackWrongImage.gameObject.SetActive(false);
|
|
|
|
UpdateScoreText();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fungsi yang dipanggil oleh tombol untuk membuka kamera bawaan dan mengambil gambar.
|
|
/// </summary>
|
|
public void TakePicture()
|
|
{
|
|
NativeCamera.Permission permission = NativeCamera.CheckPermission(true);
|
|
if (permission == NativeCamera.Permission.Denied || permission == NativeCamera.Permission.ShouldAsk)
|
|
{
|
|
NativeCamera.RequestPermission(true);
|
|
}
|
|
|
|
NativeCamera.TakePicture((path) =>
|
|
{
|
|
if (!string.IsNullOrEmpty(path))
|
|
{
|
|
// Load gambar dengan ukuran target
|
|
Texture2D texture = NativeCamera.LoadImageAtPath(path, inputWidth);
|
|
if (texture == null)
|
|
{
|
|
Debug.LogError("Gagal memuat gambar dari path: " + path);
|
|
return;
|
|
}
|
|
// Pastikan texture bisa diakses (readable)
|
|
Texture2D readableTexture = MakeTextureReadable(texture);
|
|
// Koreksi orientasi gambar dari kamera (sesuaikan rotasinya jika perlu)
|
|
Texture2D correctedTexture = CorrectCameraImage(readableTexture);
|
|
|
|
// Tampilkan gambar di UI Image (opsional)
|
|
if (displayImage != null)
|
|
{
|
|
Sprite newSprite = Sprite.Create(correctedTexture,
|
|
new Rect(0, 0, correctedTexture.width, correctedTexture.height),
|
|
new Vector2(0.5f, 0.5f));
|
|
displayImage.sprite = newSprite;
|
|
}
|
|
|
|
// Proses gambar yang diambil dengan inferensi
|
|
RunInference(correctedTexture);
|
|
}
|
|
}, maxSize: inputWidth);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Melakukan inferensi pada gambar yang diberikan.
|
|
/// </summary>
|
|
public void RunInference(Texture2D inputTexture)
|
|
{
|
|
if (inputTexture == null)
|
|
{
|
|
Debug.LogError("Gambar tidak tersedia untuk inferensi!");
|
|
return;
|
|
}
|
|
|
|
// Resize texture ke ukuran input model
|
|
Texture2D resized = ResizeTexture(inputTexture, inputWidth, inputHeight);
|
|
|
|
// Dapatkan data piksel dan lakukan normalisasi ke rentang [-1, 1]
|
|
Color32[] pixels = resized.GetPixels32();
|
|
float[] floatValues = new float[inputWidth * inputHeight * 3];
|
|
|
|
for (int i = 0; i < pixels.Length; i++)
|
|
{
|
|
floatValues[i * 3 + 0] = (pixels[i].r / 127.5f) - 1.0f;
|
|
floatValues[i * 3 + 1] = (pixels[i].g / 127.5f) - 1.0f;
|
|
floatValues[i * 3 + 2] = (pixels[i].b / 127.5f) - 1.0f;
|
|
}
|
|
|
|
// Buat tensor input dengan dimensi [1, inputHeight, inputWidth, 3]
|
|
Tensor inputTensor = new Tensor(1, inputHeight, inputWidth, 3, floatValues);
|
|
worker.Execute(inputTensor);
|
|
Tensor outputTensor = worker.PeekOutput();
|
|
|
|
float[] scores = outputTensor.ToReadOnlyArray();
|
|
int predictedIndex = ArgMax(scores);
|
|
float confidence = scores[predictedIndex];
|
|
string predictedClass = classes[predictedIndex];
|
|
|
|
Debug.Log($"Predicted Class: {predictedClass}\nConfidence: {confidence:F2}");
|
|
|
|
if (outputText != null)
|
|
{
|
|
outputText.text = $"Predicted Class: {predictedClass}\nConfidence: {confidence:F2}";
|
|
}
|
|
|
|
// Logika skor dan feedback
|
|
if (predictedClass == "organik")
|
|
{
|
|
currentScore++;
|
|
UpdateScoreText();
|
|
if (audioSource != null && correctClip != null)
|
|
{
|
|
audioSource.PlayOneShot(correctClip);
|
|
}
|
|
// Jika sudah mencapai target (5 kali)
|
|
if (currentScore >= maxScore)
|
|
{
|
|
StartCoroutine(ShowFeedback(true, true)); // benar dan final
|
|
}
|
|
else
|
|
{
|
|
StartCoroutine(ShowFeedback(true, false)); // benar tapi belum final
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (audioSource != null && wrongClip != null)
|
|
{
|
|
audioSource.PlayOneShot(wrongClip);
|
|
}
|
|
// Jika salah, tampilkan feedback dan canvas gagal (final)
|
|
StartCoroutine(ShowFeedback(false, true));
|
|
}
|
|
|
|
inputTensor.Dispose();
|
|
outputTensor.Dispose();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Coroutine untuk menampilkan image feedback (checklist atau silang) selama delay tertentu,
|
|
/// kemudian jika isFinal true, akan menampilkan canvas hasil dan (jika benar) memutar awardClip.
|
|
/// </summary>
|
|
private IEnumerator ShowFeedback(bool isCorrect, bool isFinal)
|
|
{
|
|
// Tampilkan image feedback
|
|
if (isCorrect)
|
|
{
|
|
if (feedbackCorrectImage != null)
|
|
feedbackCorrectImage.gameObject.SetActive(true);
|
|
}
|
|
else
|
|
{
|
|
if (feedbackWrongImage != null)
|
|
feedbackWrongImage.gameObject.SetActive(true);
|
|
}
|
|
|
|
// Tunggu selama feedbackDelay
|
|
yield return new WaitForSeconds(feedbackDelay);
|
|
|
|
// Sembunyikan image feedback
|
|
if (isCorrect)
|
|
{
|
|
if (feedbackCorrectImage != null)
|
|
feedbackCorrectImage.gameObject.SetActive(false);
|
|
}
|
|
else
|
|
{
|
|
if (feedbackWrongImage != null)
|
|
feedbackWrongImage.gameObject.SetActive(false);
|
|
}
|
|
|
|
// Jika isFinal true, tampilkan canvas hasil dan putar awardClip (jika benar)
|
|
if (isFinal)
|
|
{
|
|
if (isCorrect)
|
|
{
|
|
if (audioSource != null && awardClip != null)
|
|
{
|
|
audioSource.PlayOneShot(awardClip);
|
|
}
|
|
if (canvasBerhasil != null)
|
|
canvasBerhasil.SetActive(true);
|
|
}
|
|
else
|
|
{
|
|
if (canvasGagal != null)
|
|
canvasGagal.SetActive(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mengembalikan indeks elemen dengan nilai maksimum pada array.
|
|
/// </summary>
|
|
private int ArgMax(float[] array)
|
|
{
|
|
int index = 0;
|
|
float max = array[0];
|
|
for (int i = 1; i < array.Length; i++)
|
|
{
|
|
if (array[i] > max)
|
|
{
|
|
max = array[i];
|
|
index = i;
|
|
}
|
|
}
|
|
return index;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Meresize Texture2D ke ukuran yang diinginkan.
|
|
/// </summary>
|
|
private Texture2D ResizeTexture(Texture2D source, int newWidth, int newHeight)
|
|
{
|
|
RenderTexture rt = RenderTexture.GetTemporary(newWidth, newHeight);
|
|
RenderTexture.active = rt;
|
|
Graphics.Blit(source, rt);
|
|
Texture2D newTexture = new Texture2D(newWidth, newHeight);
|
|
newTexture.ReadPixels(new Rect(0, 0, newWidth, newHeight), 0, 0);
|
|
newTexture.Apply();
|
|
RenderTexture.active = null;
|
|
RenderTexture.ReleaseTemporary(rt);
|
|
return newTexture;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Memperbarui tampilan skor pada UI.
|
|
/// </summary>
|
|
private void UpdateScoreText()
|
|
{
|
|
if (scoreText != null)
|
|
{
|
|
scoreText.text = $"{currentScore}/{maxScore}";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Membuat salinan texture yang dapat diakses (readable) dengan RenderTexture.
|
|
/// </summary>
|
|
private Texture2D MakeTextureReadable(Texture2D tex)
|
|
{
|
|
RenderTexture tmp = RenderTexture.GetTemporary(tex.width, tex.height, 0, RenderTextureFormat.Default, RenderTextureReadWrite.Linear);
|
|
Graphics.Blit(tex, tmp);
|
|
RenderTexture previous = RenderTexture.active;
|
|
RenderTexture.active = tmp;
|
|
Texture2D readableTex = new Texture2D(tex.width, tex.height);
|
|
readableTex.ReadPixels(new Rect(0, 0, tmp.width, tmp.height), 0, 0);
|
|
readableTex.Apply();
|
|
RenderTexture.active = previous;
|
|
RenderTexture.ReleaseTemporary(tmp);
|
|
return readableTex;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mengoreksi orientasi gambar yang diambil dari NativeCamera.
|
|
/// Contoh: memutar gambar 90° searah jarum jam.
|
|
/// (Jika perlu, sesuaikan rotasinya sesuai kondisi perangkat.)
|
|
/// </summary>
|
|
private Texture2D CorrectCameraImage(Texture2D original)
|
|
{
|
|
int width = original.width;
|
|
int height = original.height;
|
|
Texture2D rotated = new Texture2D(height, width, original.format, false);
|
|
|
|
for (int i = 0; i < width; i++)
|
|
{
|
|
for (int j = 0; j < height; j++)
|
|
{
|
|
rotated.SetPixel(j, width - i - 1, original.GetPixel(i, j));
|
|
}
|
|
}
|
|
rotated.Apply();
|
|
return rotated;
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
worker?.Dispose();
|
|
}
|
|
}
|