diff --git a/android/.gradle/8.5/checksums/checksums.lock b/android/.gradle/8.5/checksums/checksums.lock
index 4ef611b..ae37106 100644
Binary files a/android/.gradle/8.5/checksums/checksums.lock and b/android/.gradle/8.5/checksums/checksums.lock differ
diff --git a/android/.gradle/8.5/checksums/md5-checksums.bin b/android/.gradle/8.5/checksums/md5-checksums.bin
index 6054570..36f1fc6 100644
Binary files a/android/.gradle/8.5/checksums/md5-checksums.bin and b/android/.gradle/8.5/checksums/md5-checksums.bin differ
diff --git a/android/.gradle/8.5/checksums/sha1-checksums.bin b/android/.gradle/8.5/checksums/sha1-checksums.bin
index 6bb3849..c968a6d 100644
Binary files a/android/.gradle/8.5/checksums/sha1-checksums.bin and b/android/.gradle/8.5/checksums/sha1-checksums.bin differ
diff --git a/android/.gradle/8.5/executionHistory/executionHistory.bin b/android/.gradle/8.5/executionHistory/executionHistory.bin
index 79d1c30..ec82e1f 100644
Binary files a/android/.gradle/8.5/executionHistory/executionHistory.bin and b/android/.gradle/8.5/executionHistory/executionHistory.bin differ
diff --git a/android/.gradle/8.5/executionHistory/executionHistory.lock b/android/.gradle/8.5/executionHistory/executionHistory.lock
index 21a4f51..1f3aa40 100644
Binary files a/android/.gradle/8.5/executionHistory/executionHistory.lock and b/android/.gradle/8.5/executionHistory/executionHistory.lock differ
diff --git a/android/.gradle/8.5/fileHashes/fileHashes.bin b/android/.gradle/8.5/fileHashes/fileHashes.bin
index ffa5ed6..bfa6bb8 100644
Binary files a/android/.gradle/8.5/fileHashes/fileHashes.bin and b/android/.gradle/8.5/fileHashes/fileHashes.bin differ
diff --git a/android/.gradle/8.5/fileHashes/fileHashes.lock b/android/.gradle/8.5/fileHashes/fileHashes.lock
index 45d5cde..ccb5e0e 100644
Binary files a/android/.gradle/8.5/fileHashes/fileHashes.lock and b/android/.gradle/8.5/fileHashes/fileHashes.lock differ
diff --git a/android/.gradle/8.5/fileHashes/resourceHashesCache.bin b/android/.gradle/8.5/fileHashes/resourceHashesCache.bin
index e8869ed..e2f2aab 100644
Binary files a/android/.gradle/8.5/fileHashes/resourceHashesCache.bin and b/android/.gradle/8.5/fileHashes/resourceHashesCache.bin differ
diff --git a/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock
index d350e3d..d603f86 100644
Binary files a/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ
diff --git a/android/.gradle/buildOutputCleanup/outputFiles.bin b/android/.gradle/buildOutputCleanup/outputFiles.bin
index 1e5dc95..d05cd6f 100644
Binary files a/android/.gradle/buildOutputCleanup/outputFiles.bin and b/android/.gradle/buildOutputCleanup/outputFiles.bin differ
diff --git a/android/.gradle/config.properties b/android/.gradle/config.properties
new file mode 100644
index 0000000..428649b
--- /dev/null
+++ b/android/.gradle/config.properties
@@ -0,0 +1,2 @@
+#Thu Jun 05 00:28:15 CST 2025
+java.home=/Applications/Android Studio.app/Contents/jbr/Contents/Home
diff --git a/android/.gradle/file-system.probe b/android/.gradle/file-system.probe
index 91c6f0e..74e41fc 100644
Binary files a/android/.gradle/file-system.probe and b/android/.gradle/file-system.probe differ
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 0595b7f..556abdd 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -51,6 +51,10 @@ dependencies {
implementation 'org.tensorflow:tensorflow-lite-gpu:2.5.0'
implementation 'org.tensorflow:tensorflow-lite-support:0.3.0'
+ // Glide 依赖
+ implementation 'com.github.bumptech.glide:glide:4.12.0'
+ annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
+
// Room 依赖
def room_version = "2.4.3"
implementation "androidx.room:room-runtime:$room_version"
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 4727907..d3f87af 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
package="org.tensorflow.lite.examples.poseestimation">
+
@@ -39,6 +40,18 @@
android:exported="false" />
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/AgeSelectionActivity.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/AgeSelectionActivity.kt
index 6dda173..b3ccc61 100644
--- a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/AgeSelectionActivity.kt
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/AgeSelectionActivity.kt
@@ -18,6 +18,7 @@ class AgeSelectionActivity : AppCompatActivity() {
private lateinit var nextButton: MaterialButton
private lateinit var backButton: ImageButton
private var selectedGender: String? = null
+ private var username: String? = null
private var currentAge = 25
private val minAge = 12
@@ -31,6 +32,7 @@ class AgeSelectionActivity : AppCompatActivity() {
setContentView(R.layout.activity_age_selection)
selectedGender = intent.getStringExtra("selected_gender")
+ username = intent.getStringExtra("username")
selectedAgeText = findViewById(R.id.selectedAgeText)
age1Above = findViewById(R.id.age1Above)
@@ -82,6 +84,7 @@ class AgeSelectionActivity : AppCompatActivity() {
val intent = Intent(this, WeightSelectionActivity::class.java)
intent.putExtra("selected_gender", selectedGender)
intent.putExtra("selected_age", currentAge)
+ intent.putExtra("username", username)
startActivity(intent)
finish()
}
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/DataFragment.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/DataFragment.kt
index 250c609..70e7ff1 100644
--- a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/DataFragment.kt
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/DataFragment.kt
@@ -1,17 +1,77 @@
package org.tensorflow.lite.examples.poseestimation
+import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.widget.Toast
import androidx.fragment.app.Fragment
-import org.tensorflow.lite.examples.poseestimation.R
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.tensorflow.lite.examples.poseestimation.data.AppDatabase
class DataFragment : Fragment() {
+
+ private lateinit var recyclerView: RecyclerView
+ private lateinit var adapter: VideoAnalysisAdapter
+ private var currentUsername: String? = null
+
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
+ android.util.Log.d("DataFragment", "onCreateView called")
return inflater.inflate(R.layout.fragment_data, container, false)
}
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ android.util.Log.d("DataFragment", "onViewCreated called")
+
+ // 获取当前登录用户名
+ val prefs = requireContext().getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
+ currentUsername = prefs.getString("username", null)?.trim()
+ android.util.Log.d("DataFragment", "Current Username: $currentUsername")
+
+ if (currentUsername == null) {
+ Toast.makeText(requireContext(), "未登录,无法查看数据", Toast.LENGTH_SHORT).show()
+ android.util.Log.w("DataFragment", "Current username is null, cannot load data.")
+ return
+ }
+
+ recyclerView = view.findViewById(R.id.recycler_view_video_analysis_results)
+ recyclerView.layoutManager = LinearLayoutManager(context)
+
+ loadVideoAnalysisResults()
+ }
+
+ private fun loadVideoAnalysisResults() {
+ android.util.Log.d("DataFragment", "loadVideoAnalysisResults called for username: $currentUsername")
+ currentUsername?.let { username ->
+ lifecycleScope.launch {
+ val db = AppDatabase.getDatabase(requireContext())
+ val results = withContext(Dispatchers.IO) {
+ db.videoAnalysisResultDao().getVideoAnalysisResultsByUsername(username).firstOrNull()
+ }
+
+ withContext(Dispatchers.Main) {
+ if (results != null && results.isNotEmpty()) {
+ android.util.Log.d("DataFragment", "Loaded ${results.size} video analysis results.")
+ adapter = VideoAnalysisAdapter(results)
+ recyclerView.adapter = adapter
+ } else {
+ // 没有数据,可以显示一个提示
+ Toast.makeText(requireContext(), "暂无训练记录", Toast.LENGTH_SHORT).show()
+ android.util.Log.d("DataFragment", "No video analysis results found for username: $username")
+ }
+ }
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/EditProfileActivity.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/EditProfileActivity.kt
index f36257a..77a87b7 100644
--- a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/EditProfileActivity.kt
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/EditProfileActivity.kt
@@ -3,6 +3,7 @@ package org.tensorflow.lite.examples.poseestimation
import android.app.Activity
import android.content.Context
import android.content.Intent
+import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
@@ -11,12 +12,15 @@ import android.widget.ImageView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.tensorflow.lite.examples.poseestimation.data.AppDatabase
+import org.tensorflow.lite.examples.poseestimation.data.UserProfile
class EditProfileActivity : AppCompatActivity() {
@@ -27,8 +31,24 @@ class EditProfileActivity : AppCompatActivity() {
private lateinit var btnSave: com.google.android.material.button.MaterialButton
private lateinit var btnBack: ImageView
+ // 新增的个人信息输入框
+ private lateinit var editGender: EditText
+ private lateinit var editAge: EditText
+ private lateinit var editWeight: EditText
+ private lateinit var editHeight: EditText
+
private var currentUsername: String? = null
- private var selectedImageUri: Uri? = null
+ private var selectedImageUri: Uri? = null // 存储复制到内部存储后的URI
+
+ // 用于请求READ_EXTERNAL_STORAGE权限
+ private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
+ isGranted: Boolean ->
+ if (isGranted) {
+ openGallery()
+ } else {
+ Toast.makeText(this, "需要读取存储权限才能选择头像", Toast.LENGTH_SHORT).show()
+ }
+ }
// 用于启动相册选择图片并获取结果
private val selectPhotoLauncher =
@@ -38,9 +58,15 @@ class EditProfileActivity : AppCompatActivity() {
imageUri?.let {
// 在ImageView中显示选中的图片
imageAvatar.setImageURI(it)
- selectedImageUri = it
- // TODO: 在这里添加保存头像图片的逻辑,例如保存URI或将图片复制到内部存储
- // TODO: 需要修改User或UserProfile表结构来存储头像路径
+
+ // 将图片复制到内部存储
+ lifecycleScope.launch(Dispatchers.IO) {
+ val internalUri = copyImageToInternalStorage(it)
+ withContext(Dispatchers.Main) {
+ selectedImageUri = internalUri
+ android.util.Log.d("EditProfileActivity", "Image copied to internal storage: $internalUri")
+ }
+ }
}
}
}
@@ -68,12 +94,18 @@ class EditProfileActivity : AppCompatActivity() {
btnSave = findViewById(R.id.btn_save)
btnBack = findViewById(R.id.btn_back)
+ // 绑定新增的个人信息输入框
+ editGender = findViewById(R.id.edit_gender)
+ editAge = findViewById(R.id.edit_age)
+ editWeight = findViewById(R.id.edit_weight)
+ editHeight = findViewById(R.id.edit_height)
+
// 加载并显示当前用户信息
loadUserProfile()
// 设置选择头像按钮的点击事件
btnSelectPhoto.setOnClickListener {
- openGallery()
+ checkPermissionAndOpenGallery()
}
// 设置保存按钮的点击事件
@@ -94,16 +126,37 @@ class EditProfileActivity : AppCompatActivity() {
val db = AppDatabase.getDatabase(this@EditProfileActivity)
// 使用getUserByUsername查询User对象,其中包含signature字段
val user = db.userDao().getUserByUsername(username).first()
+
+ // 查询UserProfile数据
+ val userProfile = db.userProfileDao().getUserProfileByUsername(username).firstOrNull()
+
withContext(Dispatchers.Main) {
if (user != null) {
editUsername.setText(user.username)
editSignature.setText(user.signature)
- // TODO: 添加加载用户头像的逻辑(如果已保存)
- // TODO: 需要从数据库或SharedPreferences获取头像路径并加载到imageAvatar
+ // 加载用户头像的逻辑
+ userProfile?.avatarUri?.let { uriString ->
+ try {
+ val imageUri = Uri.parse(uriString)
+ imageAvatar.setImageURI(imageUri)
+ selectedImageUri = imageUri // 确保 selectedImageUri 也更新以防再次保存
+ } catch (e: Exception) {
+ android.util.Log.e("EditProfileActivity", "Error parsing avatar URI: $uriString", e)
+ }
+ }
} else {
// 用户不存在,这通常不应该发生如果已经登录
Toast.makeText(this@EditProfileActivity, "加载用户信息失败", Toast.LENGTH_SHORT).show()
}
+
+ // 显示UserProfile数据
+ if (userProfile != null) {
+ editGender.setText(userProfile.gender)
+ // 对于 Int 类型,需要转换为 String
+ if (userProfile.age != 0) editAge.setText(userProfile.age.toString())
+ if (userProfile.weight != 0) editWeight.setText(userProfile.weight.toString())
+ if (userProfile.height != 0) editHeight.setText(userProfile.height.toString())
+ }
}
}
}
@@ -115,25 +168,83 @@ class EditProfileActivity : AppCompatActivity() {
selectPhotoLauncher.launch(galleryIntent)
}
+ // 检查权限并打开相册
+ private fun checkPermissionAndOpenGallery() {
+ when {
+ ContextCompat.checkSelfPermission(
+ this,
+ android.Manifest.permission.READ_EXTERNAL_STORAGE
+ ) == PackageManager.PERMISSION_GRANTED -> {
+ // 权限已授予
+ openGallery()
+ }
+ shouldShowRequestPermissionRationale(android.Manifest.permission.READ_EXTERNAL_STORAGE) -> {
+ // 向用户解释为什么需要这个权限,然后再次请求
+ Toast.makeText(this, "需要读取存储权限来选择您的头像", Toast.LENGTH_LONG).show()
+ requestPermissionLauncher.launch(android.Manifest.permission.READ_EXTERNAL_STORAGE)
+ }
+ else -> {
+ // 请求权限
+ requestPermissionLauncher.launch(android.Manifest.permission.READ_EXTERNAL_STORAGE)
+ }
+ }
+ }
+
+ // 将图片复制到应用内部存储并返回新URI
+ private fun copyImageToInternalStorage(uri: Uri): Uri? {
+ return try {
+ val inputStream = contentResolver.openInputStream(uri)
+ val outputFileName = "avatar_${System.currentTimeMillis()}.jpg"
+ val outputFile = java.io.File(filesDir, outputFileName)
+ val outputStream = outputFile.outputStream()
+
+ inputStream?.copyTo(outputStream)
+ inputStream?.close()
+ outputStream.close()
+ android.util.Log.d("EditProfileActivity", "Image copied to: ${outputFile.absolutePath}")
+ Uri.fromFile(outputFile)
+ } catch (e: Exception) {
+ android.util.Log.e("EditProfileActivity", "Error copying image to internal storage: ${e.message}", e)
+ null
+ }
+ }
+
// 保存修改后的个人资料
private fun saveProfile() {
currentUsername?.let { username ->
val newSignature = editSignature.text.toString().trim()
- // 用户名暂时不可编辑,所以只更新个性签名
+
+ // 获取个人信息输入框的值
+ val newGender = editGender.text.toString().trim()
+ val newAge = editAge.text.toString().trim().toIntOrNull() ?: 0
+ val newWeight = editWeight.text.toString().trim().toIntOrNull() ?: 0
+ val newHeight = editHeight.text.toString().trim().toIntOrNull() ?: 0
+
+ // 添加日志输出,确认获取到的数据和当前用户名
+ android.util.Log.d("EditProfileActivity", "Current Username: $username")
+ android.util.Log.d("EditProfileActivity", "New Signature: $newSignature")
+ android.util.Log.d("EditProfileActivity", "New Gender: $newGender, Age: $newAge, Weight: $newWeight, Height: $newHeight")
+ android.util.Log.d("EditProfileActivity", "Selected Image URI before save: $selectedImageUri")
lifecycleScope.launch(Dispatchers.IO) {
val db = AppDatabase.getDatabase(this@EditProfileActivity)
// 调用UserDao中的updateSignature方法更新个性签名
db.userDao().updateSignature(username, newSignature)
- // 保存头像URI到数据库
- db.userDao().updateAvatarUri(username, selectedImageUri?.toString())
+ // 更新UserProfile表中的数据,包括头像URI
+ val updatedProfile = UserProfile(username, newGender, newAge, newWeight, newHeight, selectedImageUri?.toString())
+ android.util.Log.d("EditProfileActivity", "Attempting to update UserProfile: $updatedProfile")
+ val rowsUpdated = db.userProfileDao().updateUserProfile(updatedProfile)
+ android.util.Log.d("EditProfileActivity", "UserProfile rows updated: $rowsUpdated for username: $username") // 记录更新的行数
withContext(Dispatchers.Main) {
- Toast.makeText(this@EditProfileActivity, "资料保存成功", Toast.LENGTH_SHORT).show()
- // 设置Result为RESULT_OK,通知SettingFragment数据已更新 (可选,取决于SettingFragment是否需要知道保存成功)
- setResult(Activity.RESULT_OK)
- finish() // 保存成功后结束当前Activity,返回Setting页面
+ if (rowsUpdated > 0) {
+ Toast.makeText(this@EditProfileActivity, "资料保存成功", Toast.LENGTH_SHORT).show()
+ setResult(Activity.RESULT_OK)
+ finish()
+ } else {
+ Toast.makeText(this@EditProfileActivity, "资料保存失败:未找到对应用户档案", Toast.LENGTH_LONG).show()
+ }
}
}
}
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/ExerciseDetailActivity.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/ExerciseDetailActivity.kt
index d4e540d..262e5d0 100644
--- a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/ExerciseDetailActivity.kt
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/ExerciseDetailActivity.kt
@@ -61,7 +61,7 @@ class ExerciseDetailActivity : AppCompatActivity() {
// 设置开始训练按钮点击事件,跳转到姿态识别页面
startTrainingButton.setOnClickListener {
- val intent = Intent(this, MainActivity::class.java)
+ val intent = Intent(this, VideoAnalysisActivity::class.java)
// 传递当前动作名称给 MainActivity
intent.putExtra("current_exercise", exerciseNameFromIntent)
startActivity(intent)
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/GenderSelectionActivity.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/GenderSelectionActivity.kt
index 202b14e..fdbaf6c 100644
--- a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/GenderSelectionActivity.kt
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/GenderSelectionActivity.kt
@@ -14,11 +14,14 @@ class GenderSelectionActivity : AppCompatActivity() {
private lateinit var femaleText: TextView
private lateinit var nextButton: MaterialButton
private var selectedGender: String? = null
+ private var username: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_gender_selection)
+ username = intent.getStringExtra("username")
+
maleButton = findViewById(R.id.maleButton)
femaleButton = findViewById(R.id.femaleButton)
maleText = findViewById(R.id.maleText)
@@ -38,9 +41,9 @@ class GenderSelectionActivity : AppCompatActivity() {
}
nextButton.setOnClickListener {
- // 跳转到年龄选择页面,并传递性别信息
val intent = Intent(this, AgeSelectionActivity::class.java)
intent.putExtra("selected_gender", selectedGender)
+ intent.putExtra("username", username)
startActivity(intent)
}
}
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/HeightSelectionActivity.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/HeightSelectionActivity.kt
index ac9121e..cad8040 100644
--- a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/HeightSelectionActivity.kt
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/HeightSelectionActivity.kt
@@ -43,8 +43,10 @@ class HeightSelectionActivity : AppCompatActivity() {
selectedGender = intent.getStringExtra("selected_gender")
selectedAge = intent.getIntExtra("selected_age", 0)
selectedWeight = intent.getIntExtra("selected_weight", 0)
- username = intent.getStringExtra("username")
+ username = intent.getStringExtra("username")?.trim()
+ android.util.Log.d("HeightSelectionActivity", "Received username: $username")
+
selectedHeightText = findViewById(R.id.selectedHeightText)
heightUnit = findViewById(R.id.heightUnit)
height1Above = findViewById(R.id.height1Above)
@@ -98,11 +100,22 @@ class HeightSelectionActivity : AppCompatActivity() {
val weight = selectedWeight
val height = currentHeight
val user = username ?: ""
+ android.util.Log.d("HeightSelectionActivity", "Attempting to save profile for username: $user, gender: $gender, age: $age, weight: $weight, height: $height")
lifecycleScope.launch(Dispatchers.IO) {
try {
val profile = UserProfile(user, gender, age, weight, height)
- db.userProfileDao().insertUserProfile(profile)
- } catch (_: Exception) {}
+ android.util.Log.d("HeightSelectionActivity", "Saving UserProfile: $profile")
+ val rowsUpdated = db.userProfileDao().updateUserProfile(profile)
+ android.util.Log.d("HeightSelectionActivity", "UserProfile rows updated: $rowsUpdated for username: $user")
+ if (rowsUpdated == 0) {
+ db.userProfileDao().insertUserProfile(profile)
+ android.util.Log.d("HeightSelectionActivity", "UserProfile inserted after update failed for username: $user")
+ } else {
+ android.util.Log.d("HeightSelectionActivity", "UserProfile updated successfully for username: $user")
+ }
+ } catch (e: Exception) {
+ android.util.Log.e("HeightSelectionActivity", "Error saving user profile for username: $user: ${e.message}", e)
+ }
}
val intent = Intent(this, LoginActivity::class.java)
intent.putExtra("selected_gender", selectedGender)
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/MainTabActivity.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/MainTabActivity.kt
index adc9a94..0b93025 100644
--- a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/MainTabActivity.kt
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/MainTabActivity.kt
@@ -4,17 +4,24 @@ import android.os.Bundle
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
+import androidx.appcompat.widget.Toolbar
+import android.widget.TextView
import org.tensorflow.lite.examples.poseestimation.R
class MainTabActivity : AppCompatActivity() {
private lateinit var navHome: ImageView
private lateinit var navData: ImageView
private lateinit var navSetting: ImageView
+ private lateinit var toolbar: Toolbar
+ private lateinit var toolbarTitle: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_tab)
+ toolbar = findViewById(R.id.toolbar)
+ toolbarTitle = findViewById(R.id.toolbar_title)
+
navHome = findViewById(R.id.nav_home)
navData = findViewById(R.id.nav_data)
navSetting = findViewById(R.id.nav_setting)
@@ -27,27 +34,28 @@ class MainTabActivity : AppCompatActivity() {
// 5. updateNavIcons 方法根据当前选中的导航项(0、1、2)更新三个导航按钮的图标资源
// 默认显示HomeFragment
- switchFragment(HomeFragment())
+ switchFragment(HomeFragment(), "形动力")
updateNavIcons(0)
navHome.setOnClickListener {
- switchFragment(HomeFragment())
+ switchFragment(HomeFragment(), "形动力")
updateNavIcons(0)
}
navData.setOnClickListener {
- switchFragment(DataFragment())
+ switchFragment(DataFragment(), "形动力")
updateNavIcons(1)
}
navSetting.setOnClickListener {
- switchFragment(SettingFragment())
+ switchFragment(SettingFragment(), "形动力")
updateNavIcons(2)
}
}
- private fun switchFragment(fragment: Fragment) {
+ private fun switchFragment(fragment: Fragment, title: String) {
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, fragment)
.commit()
+ toolbarTitle.text = title
}
private fun updateNavIcons(selected: Int) {
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/Onboarding3Fragment.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/Onboarding3Fragment.kt
index 3ad9896..df66273 100644
--- a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/Onboarding3Fragment.kt
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/Onboarding3Fragment.kt
@@ -15,11 +15,14 @@ class Onboarding3Fragment : Fragment() {
): View? {
val view = inflater.inflate(R.layout.activity_onboarding3, container, false)
+ val username = arguments?.getString("username") // 获取用户名
+
// 找到Start now按钮并设置点击事件
val startButton = view.findViewById(R.id.small_butto_container)
startButton.setOnClickListener {
// 跳转到性别选择页面
val intent = Intent(requireActivity(), GenderSelectionActivity::class.java)
+ intent.putExtra("username", username) // 传递用户名
startActivity(intent)
requireActivity().finish() // 结束当前的OnboardingActivity
}
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/OnboardingActivity.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/OnboardingActivity.kt
index aa1a487..74993ff 100644
--- a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/OnboardingActivity.kt
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/OnboardingActivity.kt
@@ -9,7 +9,9 @@ class OnboardingActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_onboarding)
+ val username = intent.getStringExtra("username")
+
val viewPager = findViewById(R.id.viewPager)
- viewPager.adapter = OnboardingAdapter(this)
+ viewPager.adapter = OnboardingAdapter(this, username)
}
}
\ No newline at end of file
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/OnboardingAdapter.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/OnboardingAdapter.kt
index 7c91147..cffa2a9 100644
--- a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/OnboardingAdapter.kt
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/OnboardingAdapter.kt
@@ -1,18 +1,23 @@
package org.tensorflow.lite.examples.poseestimation
+import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
-class OnboardingAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
+class OnboardingAdapter(activity: FragmentActivity, private val username: String?) : FragmentStateAdapter(activity) {
override fun getItemCount(): Int = 3
override fun createFragment(position: Int): Fragment {
- return when (position) {
+ val fragment = when (position) {
0 -> Onboarding1Fragment()
1 -> Onboarding2Fragment()
2 -> Onboarding3Fragment()
else -> Onboarding1Fragment()
}
+ fragment.arguments = Bundle().apply {
+ putString("username", username)
+ }
+ return fragment
}
}
\ No newline at end of file
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/SettingFragment.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/SettingFragment.kt
index c1170d2..60f0258 100644
--- a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/SettingFragment.kt
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/SettingFragment.kt
@@ -24,6 +24,7 @@ import org.tensorflow.lite.examples.poseestimation.data.UserProfile
class SettingFragment : Fragment() {
private var isPersonalInfoExpanded = false
+ private var isMyDataExpanded = false
private lateinit var tvUsername: TextView
private lateinit var tvSignature: TextView
@@ -36,6 +37,11 @@ class SettingFragment : Fragment() {
private lateinit var tvHeight: TextView
private lateinit var btnAccountInfo: TextView
private lateinit var imageAvatar: ImageView
+ private lateinit var tvTotalTrainings: TextView
+ private lateinit var tvTotalCalories: TextView
+ private lateinit var tvTotalTime: TextView
+ private lateinit var btnMyData: TextView
+ private lateinit var layoutMyData: View
private var currentUsername: String? = null
@@ -48,6 +54,8 @@ class SettingFragment : Fragment() {
if (isPersonalInfoExpanded) {
loadPersonalInfo()
}
+ // 重新加载我的数据
+ loadMyData()
}
}
@@ -86,9 +94,16 @@ class SettingFragment : Fragment() {
tvHeight = view.findViewById(R.id.tv_height)
btnAccountInfo = view.findViewById(R.id.btn_account_info)
imageAvatar = view.findViewById(R.id.image_avatar)
+ tvTotalTrainings = view.findViewById(R.id.tv_total_trainings)
+ tvTotalCalories = view.findViewById(R.id.tv_total_calories)
+ tvTotalTime = view.findViewById(R.id.tv_total_time)
+ btnMyData = view.findViewById(R.id.btn_my_data)
+ layoutMyData = view.findViewById(R.id.layout_my_data)
// 页面创建时加载并显示用户数据
loadUserData()
+ // 页面创建时加载并显示我的数据
+ layoutMyData.visibility = if (isMyDataExpanded) View.VISIBLE else View.GONE
// 账户信息点击事件,使用Launcher启动EditProfileActivity
btnAccountInfo.setOnClickListener {
@@ -106,6 +121,15 @@ class SettingFragment : Fragment() {
}
}
+ // 我的数据展开/收起
+ btnMyData.setOnClickListener {
+ isMyDataExpanded = !isMyDataExpanded
+ layoutMyData.visibility = if (isMyDataExpanded) View.VISIBLE else View.GONE
+ if (isMyDataExpanded) {
+ loadMyData()
+ }
+ }
+
// 注销
btnLogout.setOnClickListener {
prefs.edit().remove("username").apply()
@@ -123,22 +147,30 @@ class SettingFragment : Fragment() {
val user = withContext(Dispatchers.IO) {
db.userDao().getUserByUsername(username).first()
}
+ // 获取UserProfile数据
+ val userProfile = withContext(Dispatchers.IO) {
+ db.userProfileDao().getUserProfileByUsername(username).firstOrNull()
+ }
+
// 在主线程更新UI
withContext(Dispatchers.Main) {
tvUsername.text = user?.username ?: "-"
tvSignature.text = user?.signature ?: "这个人很懒,什么都没写"
// 加载并显示用户头像
- user?.avatarUri?.let { uriString ->
+ userProfile?.avatarUri?.let { uriString ->
+ android.util.Log.d("SettingFragment", "Attempting to load avatar from URI: $uriString")
try {
val imageUri = Uri.parse(uriString)
imageAvatar.setImageURI(imageUri)
+ android.util.Log.d("SettingFragment", "Avatar loaded successfully from URI: $imageUri")
} catch (e: Exception) {
// 处理URI无效或图片加载失败的情况,可以显示默认头像或日志
- e.printStackTrace()
+ android.util.Log.e("SettingFragment", "Error loading avatar from URI: $uriString", e)
imageAvatar.setImageResource(R.drawable.placeholder_image) // 显示默认头像
}
} ?: run { // 如果avatarUri为null,也显示默认头像
+ android.util.Log.d("SettingFragment", "Avatar URI is null, displaying placeholder.")
imageAvatar.setImageResource(R.drawable.placeholder_image)
}
}
@@ -162,14 +194,41 @@ class SettingFragment : Fragment() {
tvAge.text = "年龄:${profile.age}"
tvWeight.text = "体重:${profile.weight} kg"
tvHeight.text = "身高:${profile.height} cm"
+ // 添加日志输出
+ android.util.Log.d("SettingFragment", "UserProfile loaded: $profile")
} else {
tvGender.text = "性别:-"
tvAge.text = "年龄:-"
tvWeight.text = "体重:-"
tvHeight.text = "身高:-"
+ // 添加日志输出
+ android.util.Log.d("SettingFragment", "UserProfile not found for username: $username")
}
}
}
}
}
+
+ // 提取加载我的数据的逻辑
+ private fun loadMyData() {
+ currentUsername?.let { username ->
+ lifecycleScope.launch {
+ val db = AppDatabase.getDatabase(requireContext())
+ val videoAnalysisResults = withContext(Dispatchers.IO) {
+ db.videoAnalysisResultDao().getVideoAnalysisResultsByUsername(username).first()
+ }
+
+ withContext(Dispatchers.Main) {
+ val totalTrainings = videoAnalysisResults.size
+ // 简化的卡路里和时间估算 (假设每次训练消耗50kcal,每次训练平均5分钟)
+ val totalCalories = totalTrainings * 50
+ val totalMinutes = totalTrainings * 5
+
+ tvTotalTrainings.text = "总训练次数:$totalTrainings"
+ tvTotalCalories.text = "总消耗卡路里:$totalCalories kcal"
+ tvTotalTime.text = "总训练时间:$totalMinutes 分钟"
+ }
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/SignupActivity.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/SignupActivity.kt
index d6c9ce7..cffa659 100644
--- a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/SignupActivity.kt
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/SignupActivity.kt
@@ -72,15 +72,18 @@ class SignupActivity : AppCompatActivity() {
// 创建新用户
val newUser = User(username, password)
db.userDao().insertUser(newUser)
+ android.util.Log.d("SignupActivity", "User inserted: $username")
// 同步插入user_profiles表,默认值
val newProfile = UserProfile(username, "-", 0, 0, 0)
- db.userProfileDao().insertUserProfile(newProfile)
+ val profileId = db.userProfileDao().insertUserProfile(newProfile)
+ android.util.Log.d("SignupActivity", "UserProfile inserted for username: $username with ID: $profileId")
runOnUiThread {
Toast.makeText(this@SignupActivity, "注册成功", Toast.LENGTH_SHORT).show()
// 注册成功后跳转到OnboardingActivity
val intent = Intent(this@SignupActivity, OnboardingActivity::class.java)
+ intent.putExtra("username", username) // 传递用户名
startActivity(intent)
finish() // 结束注册界面
}
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/VideoAnalysisActivity.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/VideoAnalysisActivity.kt
new file mode 100644
index 0000000..5244ec3
--- /dev/null
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/VideoAnalysisActivity.kt
@@ -0,0 +1,286 @@
+package org.tensorflow.lite.examples.poseestimation
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.media.MediaMetadataRetriever
+import android.net.Uri
+import android.os.Bundle
+import android.provider.OpenableColumns
+import android.view.View
+import android.widget.Button
+import android.widget.ProgressBar
+import android.widget.TextView
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.delay
+import org.tensorflow.lite.examples.poseestimation.data.AppDatabase
+import org.tensorflow.lite.examples.poseestimation.data.VideoAnalysisResult
+import org.tensorflow.lite.examples.poseestimation.ml.MoveNet
+import org.tensorflow.lite.examples.poseestimation.ml.PoseDetector
+import org.tensorflow.lite.examples.poseestimation.ml.ModelType
+import org.tensorflow.lite.examples.poseestimation.data.Device
+import org.tensorflow.lite.examples.poseestimation.data.Person
+import org.tensorflow.lite.examples.poseestimation.data.KeyPoint
+import org.tensorflow.lite.examples.poseestimation.evaluator.DeadliftEvaluator
+import org.tensorflow.lite.examples.poseestimation.evaluator.SquatEvaluator
+import org.tensorflow.lite.examples.poseestimation.evaluator.ExerciseEvaluator
+import org.tensorflow.lite.examples.poseestimation.evaluator.PlankEvaluator
+import org.tensorflow.lite.examples.poseestimation.evaluator.PullUpEvaluator
+import org.tensorflow.lite.examples.poseestimation.evaluator.PushUpEvaluator
+
+class VideoAnalysisActivity : AppCompatActivity() {
+
+ private lateinit var tvExerciseName: TextView
+ private lateinit var btnSelectVideo: Button
+ private lateinit var tvAnalysisStatus: TextView
+ private lateinit var progressBarAnalysis: ProgressBar
+
+ private var currentExerciseType: String? = null
+ private var currentUsername: String? = null
+
+ private lateinit var poseDetector: PoseDetector
+
+ // 用于存储复制到内部存储后的视频URI
+ private var internalVideoUri: Uri? = null
+
+ // Launcher for selecting video from local storage
+ private val selectVideoLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ if (result.resultCode == Activity.RESULT_OK) {
+ val videoUri: Uri? = result.data?.data
+ videoUri?.let {
+ // 将视频复制到内部存储
+ lifecycleScope.launch(Dispatchers.IO) {
+ val copiedUri = copyVideoToInternalStorage(it)
+ withContext(Dispatchers.Main) {
+ if (copiedUri != null) {
+ internalVideoUri = copiedUri
+ android.util.Log.d("VideoAnalysisActivity", "视频已复制到内部存储: $internalVideoUri")
+ tvAnalysisStatus.text = "已选择视频: ${getFileName(it)},开始分析..."
+ tvAnalysisStatus.visibility = View.VISIBLE
+ progressBarAnalysis.visibility = View.VISIBLE
+ progressBarAnalysis.progress = 0
+ startVideoAnalysis(internalVideoUri!!, currentExerciseType)
+ } else {
+ Toast.makeText(this@VideoAnalysisActivity, "视频复制失败,请重试", Toast.LENGTH_SHORT).show()
+ tvAnalysisStatus.text = "视频复制失败"
+ tvAnalysisStatus.visibility = View.VISIBLE
+ progressBarAnalysis.visibility = View.GONE
+ }
+ }
+ }
+ } ?: run {
+ Toast.makeText(this, "未选择视频", Toast.LENGTH_SHORT).show()
+ tvAnalysisStatus.text = "未选择视频"
+ tvAnalysisStatus.visibility = View.VISIBLE
+ progressBarAnalysis.visibility = View.GONE
+ }
+ } else {
+ Toast.makeText(this, "取消选择视频", Toast.LENGTH_SHORT).show()
+ tvAnalysisStatus.text = "取消选择视频"
+ tvAnalysisStatus.visibility = View.VISIBLE
+ progressBarAnalysis.visibility = View.GONE
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_video_analysis)
+
+ // 获取当前登录用户名 (假设用SharedPreferences存储)
+ val prefs = getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
+ currentUsername = prefs.getString("username", null)?.trim()
+
+ if (currentUsername == null) {
+ Toast.makeText(this, "未登录,请重新登录", Toast.LENGTH_SHORT).show()
+ startActivity(Intent(this, LoginActivity::class.java))
+ finish()
+ return
+ }
+
+ // Get exercise name from intent
+ currentExerciseType = intent.getStringExtra("current_exercise")
+
+ // Initialize views
+ tvExerciseName = findViewById(R.id.tv_exercise_name)
+ btnSelectVideo = findViewById(R.id.btn_select_video)
+ tvAnalysisStatus = findViewById(R.id.tv_analysis_status)
+ progressBarAnalysis = findViewById(R.id.progress_bar_analysis)
+
+ // Initialize PoseDetector (using MoveNet Lightning by default)
+ poseDetector = MoveNet.create(this, Device.CPU, ModelType.Lightning)
+
+ // Set exercise name
+ tvExerciseName.text = currentExerciseType ?: "未知动作"
+
+ // Set click listener for video selection button
+ btnSelectVideo.setOnClickListener {
+ openVideoPicker()
+ }
+ }
+
+ // Override onDestroy to close the poseDetector
+ override fun onDestroy() {
+ super.onDestroy()
+ poseDetector.close()
+ }
+
+ private fun openVideoPicker() {
+ val intent = Intent(Intent.ACTION_GET_CONTENT)
+ intent.type = "video/*"
+ intent.addCategory(Intent.CATEGORY_OPENABLE)
+ selectVideoLauncher.launch(Intent.createChooser(intent, "选择视频"))
+ }
+
+ // Helper function to get file name from Uri
+ private fun getFileName(uri: Uri): String {
+ var result: String? = null
+ if (uri.scheme == "content") {
+ val cursor = contentResolver.query(uri, null, null, null, null)
+ cursor?.use {
+ if (it.moveToFirst()) {
+ val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
+ if (nameIndex != -1) {
+ result = it.getString(nameIndex)
+ }
+ }
+ }
+ }
+ if (result == null) {
+ result = uri.path
+ val cut = result?.lastIndexOf('/')
+ if (cut != -1) {
+ result = result?.substring(cut!! + 1)
+ }
+ }
+ return result ?: "未知文件"
+ }
+
+ // 将视频复制到应用内部存储并返回新URI
+ private fun copyVideoToInternalStorage(uri: Uri): Uri? {
+ return try {
+ val inputStream = contentResolver.openInputStream(uri)
+ val outputFileName = "video_${System.currentTimeMillis()}.mp4"
+ val outputFile = java.io.File(filesDir, outputFileName)
+ val outputStream = outputFile.outputStream()
+
+ inputStream?.copyTo(outputStream)
+ inputStream?.close()
+ outputStream.close()
+ android.util.Log.d("VideoAnalysisActivity", "视频已复制到: ${outputFile.absolutePath}")
+ Uri.fromFile(outputFile)
+ } catch (e: Exception) {
+ android.util.Log.e("VideoAnalysisActivity", "复制视频到内部存储出错: ${e.message}", e)
+ null
+ }
+ }
+
+ // Placeholder for video analysis logic
+ private fun startVideoAnalysis(videoUri: Uri, exerciseType: String?) {
+ val db = AppDatabase.getDatabase(applicationContext)
+ val videoAnalysisResultDao = db.videoAnalysisResultDao()
+
+ lifecycleScope.launch(Dispatchers.IO) {
+ withContext(Dispatchers.Main) {
+ tvAnalysisStatus.text = "正在初始化姿态识别模型..."
+ btnSelectVideo.isEnabled = false // Disable button during analysis
+ }
+
+ val retriever = MediaMetadataRetriever()
+ retriever.setDataSource(applicationContext, videoUri)
+ val durationMs = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0
+
+ // Process frames at a reasonable interval, e.g., 10 frames per second for analysis
+ val frameRate = 10 // frames per second
+ val frameIntervalUs = 1_000_000L / frameRate // interval in microseconds
+
+ var currentFrameTimeUs = 0L
+
+ // Initialize evaluator based on exercise type
+ val evaluator: ExerciseEvaluator? = when (exerciseType) {
+ "硬拉" -> DeadliftEvaluator()
+ "深蹲" -> SquatEvaluator()
+ "平板支撑" -> PlankEvaluator()
+ "引体向上" -> PullUpEvaluator()
+ "俯卧撑" -> PushUpEvaluator()
+ // TODO: Add evaluators for other exercises
+ else -> null
+ }
+
+ while (currentFrameTimeUs <= durationMs * 1000) { // durationMs is in milliseconds, getFrameAtTime expects microseconds
+ val bitmap = retriever.getFrameAtTime(currentFrameTimeUs, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
+
+ bitmap?.let { frameBitmap ->
+ val persons = poseDetector.estimatePoses(frameBitmap)
+ val person = persons.firstOrNull()
+
+ person?.let { p ->
+ if (p.score > 0.3) { // Only consider poses with reasonable confidence
+ // Pass keypoints to the evaluator
+ evaluator?.evaluateFrame(p.keyPoints)
+ }
+ }
+ frameBitmap.recycle() // Recycle bitmap to free memory
+ }
+
+ val progress = ((currentFrameTimeUs.toFloat() / (durationMs * 1000)) * 100).toInt()
+ withContext(Dispatchers.Main) {
+ progressBarAnalysis.progress = progress
+ tvAnalysisStatus.text = "正在分析视频帧: ${currentFrameTimeUs / 1000}ms / ${durationMs}ms"
+ }
+
+ currentFrameTimeUs += frameIntervalUs
+ }
+ retriever.release()
+
+ withContext(Dispatchers.Main) {
+ tvAnalysisStatus.text = "姿态识别完成,正在评估..."
+ }
+
+ // --- Pose Evaluation and Scoring ---
+
+ val finalScore: Float
+ val evaluation: String
+
+ if (evaluator != null) {
+ finalScore = evaluator.getFinalScore()
+ evaluation = evaluator.getFinalEvaluation(finalScore)
+ } else {
+ finalScore = 0f
+ evaluation = "暂不支持该动作的详细评估。"
+ }
+
+ // Store result in database
+ val result = VideoAnalysisResult(
+ username = currentUsername ?: "未知用户",
+ exerciseType = exerciseType ?: "未知运动",
+ videoUri = videoUri.toString(), // 使用已经复制到内部存储的URI
+ score = finalScore,
+ evaluation = evaluation,
+ timestamp = System.currentTimeMillis()
+ )
+
+ videoAnalysisResultDao.insertVideoAnalysisResult(result)
+
+ withContext(Dispatchers.Main) {
+ tvAnalysisStatus.text = "分析完成!正在显示结果..."
+ progressBarAnalysis.visibility = View.GONE
+ btnSelectVideo.isEnabled = true // Re-enable button
+
+ val intent = Intent(this@VideoAnalysisActivity, VideoAnalysisResultActivity::class.java).apply {
+ putExtra("exercise_name", currentExerciseType)
+ putExtra("score", finalScore)
+ putExtra("evaluation", evaluation)
+ }
+ startActivity(intent)
+ finish() // Finish this activity so user can't go back to it
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/VideoAnalysisAdapter.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/VideoAnalysisAdapter.kt
new file mode 100644
index 0000000..bb8c5d6
--- /dev/null
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/VideoAnalysisAdapter.kt
@@ -0,0 +1,71 @@
+package org.tensorflow.lite.examples.poseestimation
+
+import android.content.Intent
+import android.net.Uri
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import android.widget.Toast
+import androidx.recyclerview.widget.RecyclerView
+import com.bumptech.glide.Glide
+import androidx.core.content.FileProvider
+import org.tensorflow.lite.examples.poseestimation.data.VideoAnalysisResult
+import java.io.File
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+class VideoAnalysisAdapter(private val results: List) : RecyclerView.Adapter() {
+
+ class VideoAnalysisViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val tvExerciseType: TextView = itemView.findViewById(R.id.tv_exercise_type)
+ val tvScore: TextView = itemView.findViewById(R.id.tv_score)
+ val tvEvaluation: TextView = itemView.findViewById(R.id.tv_evaluation)
+ val videoThumbnail: ImageView = itemView.findViewById(R.id.video_thumbnail)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VideoAnalysisViewHolder {
+ val view = LayoutInflater.from(parent.context).inflate(R.layout.item_video_analysis_card, parent, false)
+ return VideoAnalysisViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: VideoAnalysisViewHolder, position: Int) {
+ val result = results[position]
+ holder.tvExerciseType.text = "运动类型:${result.exerciseType}"
+ holder.tvScore.text = "${result.score.toInt()} 分"
+ holder.tvEvaluation.text = "评价:${result.evaluation}"
+
+ Glide.with(holder.itemView.context)
+ .load(Uri.parse(result.videoUri))
+ .placeholder(R.drawable.placeholder_image)
+ .error(R.drawable.placeholder_image)
+ .into(holder.videoThumbnail)
+
+ android.util.Log.d("VideoAnalysisAdapter", "Loading video thumbnail for URI: ${result.videoUri}")
+
+ holder.videoThumbnail.setOnClickListener {
+ try {
+ val videoFile = File(Uri.parse(result.videoUri).path)
+ val context = holder.itemView.context
+
+ val contentUri: Uri = FileProvider.getUriForFile(
+ context,
+ "${context.packageName}.fileprovider",
+ videoFile
+ )
+
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.setDataAndType(contentUri, "video/*")
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ context.startActivity(intent)
+ } catch (e: Exception) {
+ android.util.Log.e("VideoAnalysisAdapter", "无法播放视频: ${result.videoUri}, 错误: ${e.message}", e)
+ Toast.makeText(holder.itemView.context, "无法播放视频,请检查文件或权限", Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+
+ override fun getItemCount(): Int = results.size
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/VideoAnalysisResultActivity.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/VideoAnalysisResultActivity.kt
new file mode 100644
index 0000000..5295a9a
--- /dev/null
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/VideoAnalysisResultActivity.kt
@@ -0,0 +1,30 @@
+package org.tensorflow.lite.examples.poseestimation
+
+import android.os.Bundle
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+
+class VideoAnalysisResultActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_video_analysis_result)
+
+ val tvExerciseName = findViewById(R.id.tv_result_exercise_name)
+ val tvScore = findViewById(R.id.tv_result_score)
+ val tvEvaluation = findViewById(R.id.tv_result_evaluation)
+
+ // 从 Intent 中获取数据
+ val exerciseName = intent.getStringExtra("exercise_name")
+ val score = intent.getFloatExtra("score", 0f)
+ val evaluation = intent.getStringExtra("evaluation")
+
+ // 设置数据显示
+ tvExerciseName.text = exerciseName ?: "未知动作"
+ tvScore.text = String.format("%.1f", score) // 格式化得分,保留一位小数
+ tvEvaluation.text = evaluation ?: "暂无评价"
+
+ // 设置标题 (可选)
+ supportActionBar?.title = "分析结果"
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/WeightSelectionActivity.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/WeightSelectionActivity.kt
index 1384a9b..d363e93 100644
--- a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/WeightSelectionActivity.kt
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/WeightSelectionActivity.kt
@@ -20,6 +20,7 @@ class WeightSelectionActivity : AppCompatActivity() {
private var selectedGender: String? = null
private var selectedAge: Int = 0
+ private var username: String? = null
private var currentWeight = 54
private var lastY: Float = 0f
@@ -34,6 +35,7 @@ class WeightSelectionActivity : AppCompatActivity() {
// 获取从上一个页面传递的数据
selectedGender = intent.getStringExtra("selected_gender")
selectedAge = intent.getIntExtra("selected_age", 0)
+ username = intent.getStringExtra("username")
// 初始化视图
selectedWeightText = findViewById(R.id.selectedWeightText)
@@ -87,6 +89,7 @@ class WeightSelectionActivity : AppCompatActivity() {
intent.putExtra("selected_gender", selectedGender)
intent.putExtra("selected_age", selectedAge)
intent.putExtra("selected_weight", currentWeight)
+ intent.putExtra("username", username)
startActivity(intent)
finish()
}
@@ -94,6 +97,7 @@ class WeightSelectionActivity : AppCompatActivity() {
backButton.setOnClickListener {
val intent = Intent(this, AgeSelectionActivity::class.java)
intent.putExtra("selected_gender", selectedGender)
+ intent.putExtra("username", username)
startActivity(intent)
finish()
}
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/data/AppDatabase.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/data/AppDatabase.kt
index c12e6a5..4ddc78d 100644
--- a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/data/AppDatabase.kt
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/data/AppDatabase.kt
@@ -4,16 +4,33 @@ import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
-@Database(entities = [User::class, UserProfile::class], version = 4, exportSchema = false)
+@Database(entities = [User::class, UserProfile::class, VideoAnalysisResult::class], version = 6, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun userProfileDao(): UserProfileDao
+ abstract fun videoAnalysisResultDao(): VideoAnalysisResultDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
+ private val MIGRATION_4_5 = object : Migration(4, 5) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL(
+ "CREATE TABLE IF NOT EXISTS `video_analysis_results` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `username` TEXT NOT NULL, `exerciseType` TEXT NOT NULL, `videoUri` TEXT NOT NULL, `score` REAL NOT NULL, `evaluation` TEXT NOT NULL, `timestamp` INTEGER NOT NULL)"
+ )
+ }
+ }
+
+ private val MIGRATION_5_6 = object : Migration(5, 6) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("ALTER TABLE user_profiles ADD COLUMN avatarUri TEXT")
+ }
+ }
+
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
@@ -21,7 +38,7 @@ abstract class AppDatabase : RoomDatabase() {
AppDatabase::class.java,
"app_database"
)
- .fallbackToDestructiveMigration()
+ .addMigrations(MIGRATION_4_5, MIGRATION_5_6)
.build()
INSTANCE = instance
instance
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/data/UserProfile.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/data/UserProfile.kt
index 07be1ce..e76b8df 100644
--- a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/data/UserProfile.kt
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/data/UserProfile.kt
@@ -9,5 +9,6 @@ data class UserProfile(
val gender: String,
val age: Int,
val weight: Int,
- val height: Int
+ val height: Int,
+ val avatarUri: String? = null // 新增:用户头像URI
)
\ No newline at end of file
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/data/UserProfileDao.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/data/UserProfileDao.kt
index 70456fb..cb440b5 100644
--- a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/data/UserProfileDao.kt
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/data/UserProfileDao.kt
@@ -3,12 +3,16 @@ package org.tensorflow.lite.examples.poseestimation.data
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
+import androidx.room.Update
import kotlinx.coroutines.flow.Flow
@Dao
interface UserProfileDao {
@Insert
- fun insertUserProfile(profile: UserProfile)
+ fun insertUserProfile(profile: UserProfile): Long
+
+ @Update
+ fun updateUserProfile(profile: UserProfile): Int
@Query("SELECT * FROM user_profiles WHERE username = :username")
fun getUserProfileByUsername(username: String): Flow
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/data/VideoAnalysisResult.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/data/VideoAnalysisResult.kt
new file mode 100644
index 0000000..60decaa
--- /dev/null
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/data/VideoAnalysisResult.kt
@@ -0,0 +1,15 @@
+package org.tensorflow.lite.examples.poseestimation.data
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "video_analysis_results")
+data class VideoAnalysisResult(
+ @PrimaryKey(autoGenerate = true) val id: Long = 0,
+ val username: String,
+ val exerciseType: String,
+ val videoUri: String,
+ val score: Float, // Simplified score for now
+ val evaluation: String,
+ val timestamp: Long // Timestamp for when the analysis was performed
+)
\ No newline at end of file
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/data/VideoAnalysisResultDao.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/data/VideoAnalysisResultDao.kt
new file mode 100644
index 0000000..be6778b
--- /dev/null
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/data/VideoAnalysisResultDao.kt
@@ -0,0 +1,19 @@
+package org.tensorflow.lite.examples.poseestimation.data
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.Query
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface VideoAnalysisResultDao {
+ @Insert
+ fun insertVideoAnalysisResult(result: VideoAnalysisResult): Long
+
+ @Query("SELECT * FROM video_analysis_results WHERE username = :username ORDER BY timestamp DESC")
+ fun getVideoAnalysisResultsByUsername(username: String): Flow>
+
+ // Optional: Get results for a specific exercise type
+ @Query("SELECT * FROM video_analysis_results WHERE username = :username AND exerciseType = :exerciseType ORDER BY timestamp DESC")
+ fun getVideoAnalysisResultsByUsernameAndExerciseType(username: String, exerciseType: String): Flow>
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/evaluator/DeadliftEvaluator.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/evaluator/DeadliftEvaluator.kt
new file mode 100644
index 0000000..9fa843f
--- /dev/null
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/evaluator/DeadliftEvaluator.kt
@@ -0,0 +1,239 @@
+package org.tensorflow.lite.examples.poseestimation.evaluator
+
+import org.tensorflow.lite.examples.poseestimation.data.KeyPoint
+import org.tensorflow.lite.examples.poseestimation.data.BodyPart
+import kotlin.math.abs
+import kotlin.math.roundToInt
+
+// 定义硬拉动作的阶段
+enum class DeadliftState {
+ START, // 起始姿态
+ DESCENT, // 下降过程
+ BOTTOM, // 底部姿态
+ ASCENT, // 上升过程
+ LOCKOUT // 锁定(完成)姿态
+}
+
+data class EvaluationMessage(
+ val message: String,
+ val isError: Boolean = false
+)
+
+class DeadliftEvaluator : ExerciseEvaluator {
+
+ private var currentState: DeadliftState = DeadliftState.START
+ private var totalScore: Float = 0f
+ private var frameCount: Int = 0
+ private var evaluationMessages: MutableList = mutableListOf()
+ private var repCount: Int = 0
+ private var isRepCounting: Boolean = false
+
+ // 硬拉关键角度阈值 (示例值,需要根据实际模型和标准进行调整)
+ private val HIP_ANGLE_START_MAX = 170f // 站立时髋关节角度
+ private val KNEE_ANGLE_START_MAX = 170f // 站立时膝关节角度
+ private val TORSO_ANGLE_START_MAX = 10f // 站立时躯干与垂直方向最大夹角
+
+ private val HIP_ANGLE_BOTTOM_MIN = 70f // 硬拉底部髋关节最小角度
+ private val KNEE_ANGLE_BOTTOM_MIN = 90f // 硬拉底部膝关节最小角度
+ private val TORSO_ANGLE_BOTTOM_MAX = 45f // 硬拉底部躯干与垂直方向最大夹角
+
+ private val BACK_STRAIGHT_THRESHOLD = 160f // 躯干挺直角度阈值 (肩-髋-膝角度)
+
+ // Helper function to calculate angle between three keypoints (A-B-C)
+ // B is the vertex of the angle
+ private fun calculateAngle(A: KeyPoint, B: KeyPoint, C: KeyPoint): Float {
+ // Use the abttPoints function from KeyPoint.kt
+ return B.abttPoints(A, B, C)
+ }
+
+ override fun evaluateFrame(keyPoints: List) {
+ if (keyPoints.isEmpty()) {
+ evaluationMessages.add(EvaluationMessage("未检测到关键点,无法评估。", true))
+ return
+ }
+
+ frameCount++
+
+ // 获取硬拉所需关键点
+ val leftShoulder = keyPoints.find { it.bodyPart == BodyPart.LEFT_SHOULDER }
+ val rightShoulder = keyPoints.find { it.bodyPart == BodyPart.RIGHT_SHOULDER }
+ val leftHip = keyPoints.find { it.bodyPart == BodyPart.LEFT_HIP }
+ val rightHip = keyPoints.find { it.bodyPart == BodyPart.RIGHT_HIP }
+ val leftKnee = keyPoints.find { it.bodyPart == BodyPart.LEFT_KNEE }
+ val rightKnee = keyPoints.find { it.bodyPart == BodyPart.RIGHT_KNEE }
+ val leftAnkle = keyPoints.find { it.bodyPart == BodyPart.LEFT_ANKLE }
+ val rightAnkle = keyPoints.find { it.bodyPart == BodyPart.RIGHT_ANKLE }
+
+ // 确保所有关键点都存在,如果缺失,则跳过此帧或给出警告
+ if (leftShoulder == null || rightShoulder == null || leftHip == null || rightHip == null ||
+ leftKnee == null || rightKnee == null || leftAnkle == null || rightAnkle == null) {
+ evaluationMessages.add(EvaluationMessage("关键点缺失,评估可能不准确。", true))
+ return
+ }
+
+ // 计算左右平均关键点,提高稳定性
+ val midShoulder = KeyPoint(
+ BodyPart.NOSE, // 使用 NOSE 作为占位符,因为没有一个"中肩"的 BodyPart
+ android.graphics.PointF(
+ (leftShoulder.coordinate.x + rightShoulder.coordinate.x) / 2,
+ (leftShoulder.coordinate.y + rightShoulder.coordinate.y) / 2
+ ),
+ (leftShoulder.score + rightShoulder.score) / 2
+ )
+ val midHip = KeyPoint(
+ BodyPart.NOSE, // 同样,使用 NOSE 作为占位符
+ android.graphics.PointF(
+ (leftHip.coordinate.x + rightHip.coordinate.x) / 2,
+ (leftHip.coordinate.y + rightHip.coordinate.y) / 2
+ ),
+ (leftHip.score + rightHip.score) / 2
+ )
+ val midKnee = KeyPoint(
+ BodyPart.NOSE, // 同样,使用 NOSE 作为占位符
+ android.graphics.PointF(
+ (leftKnee.coordinate.x + rightKnee.coordinate.x) / 2,
+ (leftKnee.coordinate.y + rightKnee.coordinate.y) / 2
+ ),
+ (leftKnee.score + rightKnee.score) / 2
+ )
+ val midAnkle = KeyPoint(
+ BodyPart.NOSE, // 同样,使用 NOSE 作为占位符
+ android.graphics.PointF(
+ (leftAnkle.coordinate.x + rightAnkle.coordinate.x) / 2,
+ (leftAnkle.coordinate.y + rightAnkle.coordinate.y) / 2
+ ),
+ (leftAnkle.score + rightAnkle.score) / 2
+ )
+
+
+ // 计算核心角度
+ val hipAngle = calculateAngle(midShoulder, midHip, midKnee) // 肩-髋-膝
+ val kneeAngle = calculateAngle(midHip, midKnee, midAnkle) // 髋-膝-踝
+
+ // 躯干角度 (这里简化为肩与髋的y坐标差与x坐标差的反正切,并转换为相对于垂直线的角度)
+ // 更准确的躯干角度需要更复杂的向量计算,例如通过肩-髋连线与竖直方向的夹角
+ val torsoAngle = abs(midShoulder.abtPoints(midHip).second - 90f) // 假设 midShoulder.abtPoints(midHip).second 是与Y轴的夹角,减去90得到与X轴的夹角,再取绝对值,但这里我们想要与垂直线的夹角,所以需要调整
+
+ // 简化的躯干角度:直接使用肩和髋的y坐标差与x坐标差的反正切
+ val dxTorso = midHip.coordinate.x - midShoulder.coordinate.x
+ val dyTorso = midHip.coordinate.y - midShoulder.coordinate.y
+ val torsoAngleVertical = Math.toDegrees(kotlin.math.atan2(dxTorso.toDouble(), dyTorso.toDouble())).toFloat() // 与y轴正方向的夹角
+
+ // 如果要与垂直方向的夹角,通常是 90 - atan2(dy, dx)
+ // 或者直接根据象限判断
+ val realTorsoAngle = abs(torsoAngleVertical) // 简化处理,取绝对值
+
+ var frameScore = 0f
+ var frameEvaluation = ""
+
+ when (currentState) {
+ DeadliftState.START -> {
+ // 评估起始姿态
+ if (hipAngle > HIP_ANGLE_START_MAX && kneeAngle > KNEE_ANGLE_START_MAX && realTorsoAngle < TORSO_ANGLE_START_MAX) {
+ frameEvaluation = "很棒的起始姿态!准备好向下,让身体进入训练模式。"
+ isRepCounting = false // 重置,等待下一次下降
+ currentState = DeadliftState.DESCENT
+ frameScore = 300f
+ } else {
+ frameEvaluation = "小提示:起始姿态可以再调整一下。试着站得更直,核心收得更紧,为硬拉做好准备!"
+ evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
+ }
+ }
+ DeadliftState.DESCENT -> {
+ // 评估下降过程
+ if (hipAngle < HIP_ANGLE_START_MAX && kneeAngle < KNEE_ANGLE_START_MAX) { // 髋和膝盖开始弯曲
+ if (realTorsoAngle < BACK_STRAIGHT_THRESHOLD) { // 检查背部是否挺直
+ frameEvaluation = "下降得很稳!记得保持背部挺直,感受髋部的发力。"
+ currentState = DeadliftState.BOTTOM
+ frameScore = 300f
+ } else {
+ frameEvaluation = "下降时背部有点弓起哦!试着挺胸收腹,保持脊柱中立,会更安全有效。"
+ evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
+ }
+ } else {
+ frameEvaluation = "继续向下,保持动作流畅,感受肌肉的拉伸。"
+ }
+ }
+ DeadliftState.BOTTOM -> {
+ // 评估底部姿态
+ if (hipAngle >= HIP_ANGLE_BOTTOM_MIN && hipAngle <= HIP_ANGLE_START_MAX &&
+ kneeAngle >= KNEE_ANGLE_BOTTOM_MIN && kneeAngle <= KNEE_ANGLE_START_MAX &&
+ realTorsoAngle <= TORSO_ANGLE_BOTTOM_MAX) {
+ frameEvaluation = "底部姿态非常稳定!现在准备向上发力,让身体爆发起来!"
+ currentState = DeadliftState.ASCENT
+ frameScore = 450f
+ } else {
+ frameEvaluation = "底部姿态可以再优化一下。尝试让髋部再低一些,膝盖不要超过脚尖太多,同时保持背部挺直。你离完美不远了!"
+ if (hipAngle < HIP_ANGLE_BOTTOM_MIN) evaluationMessages.add(EvaluationMessage("髋部下降不足。", true))
+ if (kneeAngle < KNEE_ANGLE_BOTTOM_MIN) evaluationMessages.add(EvaluationMessage("膝盖弯曲不足。", true))
+ if (realTorsoAngle > TORSO_ANGLE_BOTTOM_MAX) evaluationMessages.add(EvaluationMessage("注意!你的背部有些弓起或者过度前倾了。记得整个过程都要保持背部挺直,这是硬拉的关键!", true))
+ }
+ }
+ DeadliftState.ASCENT -> {
+ // 评估上升过程
+ if (hipAngle > HIP_ANGLE_BOTTOM_MIN && kneeAngle > KNEE_ANGLE_BOTTOM_MIN) { // 髋和膝盖开始伸展
+ if (realTorsoAngle < BACK_STRAIGHT_THRESHOLD) { // 检查背部是否挺直
+ frameEvaluation = "向上发力非常有力!继续保持背部挺直,让髋部和膝盖同步伸展,爆发力十足!"
+ currentState = DeadliftState.LOCKOUT
+ frameScore = 300f
+ } else {
+ frameEvaluation = "上升时背部有些弓起了。集中注意力,保持背部挺直,用臀部发力带动身体向上。"
+ evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
+ }
+ } else {
+ frameEvaluation = "加油,继续向上,把身体完全推回起始位置!"
+ }
+ }
+ DeadliftState.LOCKOUT -> {
+ // 评估锁定姿态
+ if (hipAngle > HIP_ANGLE_START_MAX - 5 && kneeAngle > KNEE_ANGLE_START_MAX - 5 && realTorsoAngle < TORSO_ANGLE_START_MAX + 5) {
+ frameEvaluation = "完美完成一次!锁定姿态非常棒,全身收紧,力量感十足!"
+ if (!isRepCounting) {
+ repCount++
+ isRepCounting = true
+ evaluationMessages.add(EvaluationMessage("恭喜你,又完成了一次硬拉!目前累计完成了 $repCount 次,继续保持!"))
+ }
+ currentState = DeadliftState.START // 完成一次后回到起始状态,准备下一次
+ frameScore = 150f
+ } else {
+ frameEvaluation = "快完成了!在顶部时,记得让髋部和膝盖完全伸展,轻轻收紧臀部,肩膀向后收拢,充分锁定。"
+ if (hipAngle < HIP_ANGLE_START_MAX - 5 || kneeAngle < KNEE_ANGLE_START_MAX - 5) evaluationMessages.add(EvaluationMessage("顶部锁定还不够充分哦,髋部和膝盖可以再伸展一点。", true))
+ if (realTorsoAngle > TORSO_ANGLE_START_MAX + 5) evaluationMessages.add(EvaluationMessage("注意了!在锁定阶段,身体可能有点过度后仰或者背部没有完全挺直。保持核心收紧,稳稳地完成动作。", true))
+ }
+ }
+ }
+ totalScore += frameScore // 累加每帧分数
+ evaluationMessages.add(EvaluationMessage(frameEvaluation)) // 添加每帧的评估
+ }
+
+ override fun getFinalScore(): Float {
+ val rawScore = if (frameCount > 0) totalScore / frameCount else 0f
+ val roundedScore = (rawScore / 10.0f).roundToInt() * 10.0f
+ return roundedScore.coerceIn(0f, 100f)
+ }
+
+ override fun getFinalEvaluation(finalScore: Float): String {
+ val uniqueMessages = evaluationMessages.map { it.message }.distinct().joinToString("\n")
+ val errors = evaluationMessages.filter { it.isError }.map { it.message }.distinct()
+
+ val overallEvaluationBuilder = StringBuilder()
+
+ if (errors.isEmpty()) {
+ when (finalScore.toInt()) {
+ in 90..100 -> overallEvaluationBuilder.append("太棒了!你的硬拉动作几乎完美无瑕,姿态标准,力量十足!继续保持!")
+ in 70..89 -> overallEvaluationBuilder.append("非常不错的硬拉!动作基本流畅,姿态也比较到位,再稍加注意细节就能更完美!")
+ in 50..69 -> overallEvaluationBuilder.append("硬拉动作有进步空间哦!虽然有些地方做得不错,但还需要多练习,让姿态更稳定、发力更集中。")
+ in 30..49 -> overallEvaluationBuilder.append("本次硬拉需要更多练习。动作中存在一些明显的姿态问题,这会影响训练效果和安全性。")
+ else -> overallEvaluationBuilder.append("硬拉动作仍需大量改进。请务必仔细对照标准,从基础开始练习,避免受伤。")
+ }
+ } else {
+ overallEvaluationBuilder.append("本次硬拉分析完成!发现了一些可以改进的地方:\n")
+ overallEvaluationBuilder.append(errors.joinToString("\n"))
+ }
+
+ overallEvaluationBuilder.append("\n\n以下是本次训练的详细分析过程,希望能帮助你更好地理解和改进:\n")
+ overallEvaluationBuilder.append(uniqueMessages)
+
+ return overallEvaluationBuilder.toString()
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/evaluator/ExerciseEvaluator.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/evaluator/ExerciseEvaluator.kt
new file mode 100644
index 0000000..ca3443d
--- /dev/null
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/evaluator/ExerciseEvaluator.kt
@@ -0,0 +1,9 @@
+package org.tensorflow.lite.examples.poseestimation.evaluator
+
+import org.tensorflow.lite.examples.poseestimation.data.KeyPoint
+
+interface ExerciseEvaluator {
+ fun evaluateFrame(keyPoints: List)
+ fun getFinalScore(): Float
+ fun getFinalEvaluation(finalScore: Float): String
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/evaluator/PlankEvaluator.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/evaluator/PlankEvaluator.kt
new file mode 100644
index 0000000..60ed828
--- /dev/null
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/evaluator/PlankEvaluator.kt
@@ -0,0 +1,198 @@
+package org.tensorflow.lite.examples.poseestimation.evaluator
+
+import org.tensorflow.lite.examples.poseestimation.data.KeyPoint
+import org.tensorflow.lite.examples.poseestimation.data.BodyPart
+import kotlin.math.abs
+import kotlin.math.roundToInt
+
+// 定义平板支撑动作的阶段
+enum class PlankState {
+ HOLDING, // 保持姿态
+ ERROR // 错误姿态
+}
+
+class PlankEvaluator : ExerciseEvaluator {
+
+ private var currentState: PlankState = PlankState.HOLDING
+ private var totalScore: Float = 0f
+ private var frameCount: Int = 0
+ private var evaluationMessages: MutableList = mutableListOf()
+ private var timeInCorrectPoseMs: Long = 0L // 记录正确姿态的持续时间
+ private var lastFrameTimestamp: Long = 0L
+
+ // 平板支撑关键角度阈值 (示例值,需要根据实际模型和标准进行调整)
+ private val HIP_SHOULDER_ANKLE_MIN = 160f // 髋-肩-踝角度,用于判断身体是否呈直线
+ private val HIP_SHOULDER_ANKLE_MAX = 185f // 允许略微的弧度
+ private val HIP_SAG_THRESHOLD_PERCENT = 0.05f // 臀部下沉阈值(相对于肩-踝距离的百分比)
+ private val HIP_PIKE_THRESHOLD_PERCENT = 0.05f // 臀部过高阈值
+
+ // Helper function to calculate angle between three keypoints (A-B-C)
+ // B is the vertex of the angle
+ private fun calculateAngle(A: KeyPoint, B: KeyPoint, C: KeyPoint): Float {
+ return B.abttPoints(A, B, C)
+ }
+
+ override fun evaluateFrame(keyPoints: List) {
+ if (keyPoints.isEmpty()) {
+ evaluationMessages.add(EvaluationMessage("未检测到关键点,无法评估。", true))
+ return
+ }
+
+ frameCount++
+ val currentTimestamp = System.currentTimeMillis() // 假设每帧的时间间隔可以通过外部传入或自行估算
+ if (lastFrameTimestamp != 0L) {
+ val deltaTimeMs = currentTimestamp - lastFrameTimestamp
+ // 假设视频帧率稳定,或者直接使用帧间隔(例如100ms一帧)
+ // 这里简化处理,直接使用一个固定的帧分数,而不是基于时间
+ }
+ lastFrameTimestamp = currentTimestamp
+
+ // 获取平板支撑所需关键点
+ val leftShoulder = keyPoints.find { it.bodyPart == BodyPart.LEFT_SHOULDER }
+ val rightShoulder = keyPoints.find { it.bodyPart == BodyPart.RIGHT_SHOULDER }
+ val leftHip = keyPoints.find { it.bodyPart == BodyPart.LEFT_HIP }
+ val rightHip = keyPoints.find { it.bodyPart == BodyPart.RIGHT_HIP }
+ val leftAnkle = keyPoints.find { it.bodyPart == BodyPart.LEFT_ANKLE }
+ val rightAnkle = keyPoints.find { it.bodyPart == BodyPart.RIGHT_ANKLE }
+ val nose = keyPoints.find { it.bodyPart == BodyPart.NOSE }
+
+ // 确保所有关键点都存在
+ if (leftShoulder == null || rightShoulder == null || leftHip == null || rightHip == null ||
+ leftAnkle == null || rightAnkle == null || nose == null) {
+ evaluationMessages.add(EvaluationMessage("关键点缺失,评估可能不准确。", true))
+ return
+ }
+
+ // 计算左右平均关键点,提高稳定性
+ val midShoulder = KeyPoint(
+ BodyPart.NOSE,
+ android.graphics.PointF(
+ (leftShoulder.coordinate.x + rightShoulder.coordinate.x) / 2,
+ (leftShoulder.coordinate.y + rightShoulder.coordinate.y) / 2
+ ),
+ (leftShoulder.score + rightShoulder.score) / 2
+ )
+ val midHip = KeyPoint(
+ BodyPart.NOSE,
+ android.graphics.PointF(
+ (leftHip.coordinate.x + rightHip.coordinate.x) / 2,
+ (leftHip.coordinate.y + rightHip.coordinate.y) / 2
+ ),
+ (leftHip.score + rightHip.score) / 2
+ )
+ val midAnkle = KeyPoint(
+ BodyPart.NOSE,
+ android.graphics.PointF(
+ (leftAnkle.coordinate.x + rightAnkle.coordinate.x) / 2,
+ (leftAnkle.coordinate.y + rightAnkle.coordinate.y) / 2
+ ),
+ (leftAnkle.score + rightAnkle.score) / 2
+ )
+
+ // 计算身体直线角度(肩-髋-踝)
+ val bodyLineAngle = calculateAngle(midShoulder, midHip, midAnkle)
+
+ // 计算头部姿态 (肩-鼻-肩 角度,或直接看鼻子的Y坐标相对肩的Y坐标)
+ val headY = nose.coordinate.y
+ val shoulderY = midShoulder.coordinate.y
+ var headDrop = false
+ if (headY > shoulderY + 20) { // 假设头部Y坐标显著低于肩部Y坐标视为下垂
+ headDrop = true
+ }
+
+ var frameScore = 0f
+ var frameEvaluation = ""
+ var isCorrectPose = true
+
+ // 新增臀部位置检查
+ val ankleY = midAnkle.coordinate.y
+ val hipY = midHip.coordinate.y
+
+ // 计算肩部到踝部的垂直距离
+ val verticalDistanceShoulderAnkle = abs(shoulderY - ankleY)
+ val hipSagThreshold = verticalDistanceShoulderAnkle * HIP_SAG_THRESHOLD_PERCENT
+ val hipPikeThreshold = verticalDistanceShoulderAnkle * HIP_PIKE_THRESHOLD_PERCENT
+
+ // 假设理想的髋部Y坐标应该在肩部和踝部的Y坐标之间,或者与其中一个大致平齐
+ // 更精确地,我们假设身体呈直线时,髋部Y坐标应该与肩部和踝部连线的Y坐标在同一直线上
+ // 我们可以计算肩部和踝部连线在髋部X坐标处的Y值
+ val expectedHipY = shoulderY + (ankleY - shoulderY) * ((midHip.coordinate.x - midShoulder.coordinate.x) / (midAnkle.coordinate.x - midShoulder.coordinate.x))
+
+ var hipSag = false
+ var hipPike = false
+
+ if (hipY > expectedHipY + hipSagThreshold) { // 臀部Y坐标低于预期,表示下沉
+ hipSag = true
+ isCorrectPose = false
+ evaluationMessages.add(EvaluationMessage("臀部下沉了!收紧核心,将臀部向上抬起,保持身体的平直。", true))
+ } else if (hipY < expectedHipY - hipPikeThreshold) { // 臀部Y坐标高于预期,表示过高
+ hipPike = true
+ isCorrectPose = false
+ evaluationMessages.add(EvaluationMessage("臀部抬得太高了!尝试放低臀部,让身体呈一条直线。", true))
+ }
+
+ // 评估身体是否呈直线 (现在只关注角度,垂直位置由上面新的逻辑处理)
+ if (bodyLineAngle !in HIP_SHOULDER_ANKLE_MIN..HIP_SHOULDER_ANKLE_MAX) {
+ isCorrectPose = false
+ evaluationMessages.add(EvaluationMessage("身体不够平直,请调整髋部位置。", true))
+ }
+
+ // 评估头部姿态
+ if (headDrop) {
+ isCorrectPose = false
+ evaluationMessages.add(EvaluationMessage("头部下垂,请保持颈部中立,眼睛看向地面。", true))
+ }
+
+ if (isCorrectPose) {
+ frameEvaluation = "姿态保持良好,身体呈一条直线。"
+ frameScore = 100f // 正确姿态每帧获得固定分数
+ timeInCorrectPoseMs += (currentTimestamp - lastFrameTimestamp) // 累加正确姿态时间
+ } else {
+ frameEvaluation = "请调整姿态,注意身体的直线。"
+ frameScore = 0f
+ currentState = PlankState.ERROR // 进入错误姿态状态
+ }
+
+ totalScore += frameScore
+ evaluationMessages.add(EvaluationMessage(frameEvaluation))
+
+ // 检查状态转换,如果从错误姿态恢复到正确姿态,则重新设置为HOLDING
+ if (currentState == PlankState.ERROR && isCorrectPose) {
+ currentState = PlankState.HOLDING
+ }
+ }
+
+ override fun getFinalScore(): Float {
+ // 平板支撑的最终分数更侧重于保持正确姿态的时长
+ // 这里简化为正确姿态帧数占总帧数的比例,并四舍五入到10的倍数
+ val rawScore = if (frameCount > 0) (totalScore / frameCount) else 0f
+ val roundedScore = (rawScore / 10.0f).roundToInt() * 10.0f
+ return roundedScore.coerceIn(0f, 100f)
+ }
+
+ override fun getFinalEvaluation(finalScore: Float): String {
+ val uniqueMessages = evaluationMessages.map { it.message }.distinct().joinToString("\n")
+ val errors = evaluationMessages.filter { it.isError }.map { it.message }.distinct()
+
+ val overallEvaluationBuilder = StringBuilder()
+
+ if (errors.isEmpty()) {
+ when (finalScore.toInt()) {
+ in 90..100 -> overallEvaluationBuilder.append("太棒了!你的平板支撑姿态几乎完美无瑕,身体呈一条直线,核心非常稳定!")
+ in 70..89 -> overallEvaluationBuilder.append("非常不错的平板支撑!姿态基本保持良好,再稍加注意细节就能更完美!")
+ in 50..69 -> overallEvaluationBuilder.append("平板支撑姿态有进步空间哦!虽然有些地方做得不错,但还需要多练习,让身体更稳定、更平直。")
+ in 30..49 -> overallEvaluationBuilder.append("本次平板支撑需要更多练习。姿态中存在一些明显的姿态问题,这会影响训练效果。")
+ else -> overallEvaluationBuilder.append("平板支撑姿态仍需大量改进。请务必仔细对照标准,从基础开始练习,避免受伤。")
+ }
+ } else {
+ overallEvaluationBuilder.append("本次平板支撑分析完成!发现了一些可以改进的地方:\n")
+ overallEvaluationBuilder.append(errors.joinToString("\n"))
+ // 对于平板支撑,时间更重要,但目前没有直接累加,可以考虑后期优化
+ }
+
+ overallEvaluationBuilder.append("\n\n以下是本次训练的详细分析过程,希望能帮助你更好地理解和改进:\n")
+ overallEvaluationBuilder.append(uniqueMessages)
+
+ return overallEvaluationBuilder.toString()
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/evaluator/PullUpEvaluator.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/evaluator/PullUpEvaluator.kt
new file mode 100644
index 0000000..db4289f
--- /dev/null
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/evaluator/PullUpEvaluator.kt
@@ -0,0 +1,240 @@
+package org.tensorflow.lite.examples.poseestimation.evaluator
+
+import org.tensorflow.lite.examples.poseestimation.data.KeyPoint
+import org.tensorflow.lite.examples.poseestimation.data.BodyPart
+import kotlin.math.abs
+import kotlin.math.roundToInt
+
+// 定义引体向上动作的阶段
+enum class PullUpState {
+ START, // 起始姿态(完全下放)
+ ASCENT, // 上拉过程
+ TOP, // 顶部姿态(下巴过杠)
+ DESCENT, // 下放过程
+ LOCKOUT // 恢复到起始姿态
+}
+
+class PullUpEvaluator : ExerciseEvaluator {
+
+ private var currentState: PullUpState = PullUpState.START
+ private var totalScore: Float = 0f
+ private var frameCount: Int = 0
+ private var evaluationMessages: MutableList = mutableListOf()
+ private var repCount: Int = 0
+ private var isRepCounting: Boolean = false
+
+ // 引体向上关键角度阈值 (示例值,需要根据实际模型和标准进行调整)
+ private val ELBOW_ANGLE_START_MIN = 170f // 起始姿态(手臂伸直,进一步放宽要求)
+ private val SHOULDER_ELBOW_WRIST_TOP_MAX = 80f // 顶部姿态(手肘弯曲程度,进一步放宽要求)
+ private val NOSE_BAR_HEIGHT_THRESHOLD = 0f // 鼻子与杠的相对高度(需要通过实际坐标判断)
+ private val BACK_STRAIGHT_THRESHOLD = 160f // 躯干挺直角度阈值 (肩-髋-膝角度,用于判断背部是否弓起)
+
+ // Helper function to calculate angle between three keypoints (A-B-C)
+ // B is the vertex of the angle
+ private fun calculateAngle(A: KeyPoint, B: KeyPoint, C: KeyPoint): Float {
+ return B.abttPoints(A, B, C)
+ }
+
+ override fun evaluateFrame(keyPoints: List) {
+ if (keyPoints.isEmpty()) {
+ evaluationMessages.add(EvaluationMessage("未检测到关键点,无法评估。", true))
+ return
+ }
+
+ frameCount++
+
+ // 获取引体向上所需关键点
+ val leftShoulder = keyPoints.find { it.bodyPart == BodyPart.LEFT_SHOULDER }
+ val rightShoulder = keyPoints.find { it.bodyPart == BodyPart.RIGHT_SHOULDER }
+ val leftElbow = keyPoints.find { it.bodyPart == BodyPart.LEFT_ELBOW }
+ val rightElbow = keyPoints.find { it.bodyPart == BodyPart.RIGHT_ELBOW }
+ val leftWrist = keyPoints.find { it.bodyPart == BodyPart.LEFT_WRIST }
+ val rightWrist = keyPoints.find { it.bodyPart == BodyPart.RIGHT_WRIST }
+ val nose = keyPoints.find { it.bodyPart == BodyPart.NOSE }
+ val leftHip = keyPoints.find { it.bodyPart == BodyPart.LEFT_HIP }
+ val rightHip = keyPoints.find { it.bodyPart == BodyPart.RIGHT_HIP }
+ val leftKnee = keyPoints.find { it.bodyPart == BodyPart.LEFT_KNEE }
+ val rightKnee = keyPoints.find { it.bodyPart == BodyPart.RIGHT_KNEE }
+
+ // 确保所有关键点都存在
+ if (leftShoulder == null || rightShoulder == null || leftElbow == null || rightElbow == null ||
+ leftWrist == null || rightWrist == null || nose == null || leftHip == null || rightHip == null ||
+ leftKnee == null || rightKnee == null) {
+ evaluationMessages.add(EvaluationMessage("关键点缺失,评估可能不准确。", true))
+ return
+ }
+
+ // 计算左右平均关键点,提高稳定性
+ val midShoulder = KeyPoint(
+ BodyPart.NOSE,
+ android.graphics.PointF(
+ (leftShoulder.coordinate.x + rightShoulder.coordinate.x) / 2,
+ (leftShoulder.coordinate.y + rightShoulder.coordinate.y) / 2
+ ),
+ (leftShoulder.score + rightShoulder.score) / 2
+ )
+ val midElbow = KeyPoint(
+ BodyPart.NOSE,
+ android.graphics.PointF(
+ (leftElbow.coordinate.x + rightElbow.coordinate.x) / 2,
+ (leftElbow.coordinate.y + rightElbow.coordinate.y) / 2
+ ),
+ (leftElbow.score + rightElbow.score) / 2
+ )
+ val midWrist = KeyPoint(
+ BodyPart.NOSE,
+ android.graphics.PointF(
+ (leftWrist.coordinate.x + rightWrist.coordinate.x) / 2,
+ (leftWrist.coordinate.y + rightWrist.coordinate.y) / 2
+ ),
+ (leftWrist.score + rightWrist.score) / 2
+ )
+ val midHip = KeyPoint(
+ BodyPart.NOSE,
+ android.graphics.PointF(
+ (leftHip.coordinate.x + rightHip.coordinate.x) / 2,
+ (leftHip.coordinate.y + rightHip.coordinate.y) / 2
+ ),
+ (leftHip.score + rightHip.score) / 2
+ )
+ val midKnee = KeyPoint(
+ BodyPart.NOSE,
+ android.graphics.PointF(
+ (leftKnee.coordinate.x + rightKnee.coordinate.x) / 2,
+ (leftKnee.coordinate.y + rightKnee.coordinate.y) / 2
+ ),
+ (leftKnee.score + rightKnee.score) / 2
+ )
+
+ // 计算核心角度
+ val elbowAngle = calculateAngle(midShoulder, midElbow, midWrist) // 肩-肘-腕
+ val torsoAngle = calculateAngle(midShoulder, midHip, midKnee) // 肩-髋-膝,用于评估背部姿态
+
+ // 假设杠的位置,这里需要根据实际情况调整,或者通过用户输入来设定
+ // 这里暂时用一个简化的逻辑:假设杠在肩部上方一定距离,通过鼻子的Y坐标与肩的Y坐标判断
+ val isChinOverBar = nose.coordinate.y < midWrist.coordinate.y + 0 // 鼻子的Y坐标不低于手腕Y坐标(大幅放宽)
+
+ var frameScore = 0f
+ var frameEvaluation = ""
+
+ when (currentState) {
+ PullUpState.START -> {
+ // 评估起始姿态 (手臂完全伸直)
+ if (elbowAngle > ELBOW_ANGLE_START_MIN - 15) { // 进一步放宽手臂伸直要求
+ frameEvaluation = "起始姿态良好,手臂基本伸直,准备上拉。"
+ isRepCounting = false
+ currentState = PullUpState.ASCENT
+ frameScore = 250f // 适当提高分数
+ } else {
+ frameEvaluation = "请基本伸直手臂,回到起始位置。"
+ evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
+ }
+ }
+ PullUpState.ASCENT -> {
+ // 评估上拉过程
+ if (elbowAngle < ELBOW_ANGLE_START_MIN - 10) { // 手肘开始弯曲,与起始角度保持衔接
+ if (torsoAngle > BACK_STRAIGHT_THRESHOLD) { // 检查背部是否挺直
+ if (isChinOverBar) { // 下巴过杠
+ frameEvaluation = "上拉有力,下巴已过杠,非常棒!"
+ currentState = PullUpState.TOP
+ frameScore = 450f // 适当提高分数
+ } else {
+ frameEvaluation = "继续上拉,努力让下巴过杠。"
+ evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
+ }
+ } else {
+ frameEvaluation = "上拉时背部有点弓起。请保持背部挺直。"
+ evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
+ }
+ } else {
+ frameEvaluation = "继续上拉,感受背部和手臂的发力。"
+ }
+ }
+ PullUpState.TOP -> {
+ // 评估顶部姿态 (下巴过杠,手肘弯曲)
+ if (isChinOverBar && elbowAngle < SHOULDER_ELBOW_WRIST_TOP_MAX + 30 && torsoAngle > BACK_STRAIGHT_THRESHOLD) { // 进一步放宽手肘弯曲和躯干角度
+ frameEvaluation = "完美!下巴过杠,顶部姿态保持得很好。"
+ currentState = PullUpState.DESCENT
+ frameScore = 700f // 适当提高分数
+ } else {
+ if (!isChinOverBar) evaluationMessages.add(EvaluationMessage("顶部锁定不充分,下巴未过杠。", true))
+ if (elbowAngle >= SHOULDER_ELBOW_WRIST_TOP_MAX + 30) evaluationMessages.add(EvaluationMessage("手肘弯曲不足,请再用力拉高。", true))
+ if (torsoAngle <= BACK_STRAIGHT_THRESHOLD) evaluationMessages.add(EvaluationMessage("顶部姿态背部弓起。", true))
+ frameEvaluation = "顶部姿态可以保持更稳定,确保下巴完全过杠。"
+ evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
+ }
+ }
+ PullUpState.DESCENT -> {
+ // 评估下放过程
+ if (elbowAngle > SHOULDER_ELBOW_WRIST_TOP_MAX + 10) { // 手肘开始伸直,与顶部角度保持衔接
+ if (torsoAngle > BACK_STRAIGHT_THRESHOLD) { // 检查背部是否挺直
+ if (elbowAngle > ELBOW_ANGLE_START_MIN - 20) { // 进一步放宽回到起始位置的要求
+ frameEvaluation = "下放得很稳,即将完成一次引体向上。"
+ currentState = PullUpState.LOCKOUT
+ frameScore = 450f // 适当提高分数
+ } else {
+ frameEvaluation = "继续缓慢下放,控制好身体。"
+ }
+ } else {
+ frameEvaluation = "下放时背部有点弓起。请保持背部挺直。"
+ evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
+ }
+ } else {
+ frameEvaluation = "继续下放,感受背部肌肉的拉伸。"
+ }
+ }
+ PullUpState.LOCKOUT -> {
+ // 评估完全下放(锁定)姿态
+ if (elbowAngle > ELBOW_ANGLE_START_MIN - 20 && torsoAngle > BACK_STRAIGHT_THRESHOLD) { // 进一步放宽手臂伸直要求
+ frameEvaluation = "完美完成一次引体向上!手臂基本伸直,为下一次做准备。"
+ if (!isRepCounting) {
+ repCount++
+ isRepCounting = true
+ evaluationMessages.add(EvaluationMessage("恭喜你,又完成了一次引体向上!目前累计完成了 $repCount 次,继续保持!"))
+ }
+ currentState = PullUpState.START // 完成一次后回到起始状态,准备下一次
+ frameScore = 250f // 适当提高分数
+ } else {
+ if (elbowAngle < ELBOW_ANGLE_START_MIN - 20) evaluationMessages.add(EvaluationMessage("手臂未完全伸直,请确保回到起始位置。", true))
+ if (torsoAngle <= BACK_STRAIGHT_THRESHOLD) evaluationMessages.add(EvaluationMessage("锁定姿态背部弓起。", true))
+ frameEvaluation = "请完成锁定:手臂基本伸直,身体保持平直。"
+ evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
+ }
+ }
+ }
+ totalScore += frameScore
+ evaluationMessages.add(EvaluationMessage(frameEvaluation))
+ }
+
+ override fun getFinalScore(): Float {
+ val rawScore = if (frameCount > 0) totalScore / frameCount else 0f
+ val roundedScore = (rawScore / 10.0f).roundToInt() * 10.0f
+ return roundedScore.coerceIn(0f, 100f)
+ }
+
+ override fun getFinalEvaluation(finalScore: Float): String {
+ val uniqueMessages = evaluationMessages.map { it.message }.distinct().joinToString("\n")
+ val errors = evaluationMessages.filter { it.isError }.map { it.message }.distinct()
+
+ val overallEvaluationBuilder = StringBuilder()
+
+ if (errors.isEmpty()) {
+ when (finalScore.toInt()) {
+ in 90..100 -> overallEvaluationBuilder.append("太棒了!你的引体向上动作几乎完美无瑕,姿态标准,力量十足!继续保持!")
+ in 70..89 -> overallEvaluationBuilder.append("非常不错的引体向上!动作基本流畅,姿态也比较到位,再稍加注意细节就能更完美!")
+ in 50..69 -> overallEvaluationBuilder.append("引体向上动作有进步空间哦!虽然有些地方做得不错,但还需要多练习,让姿态更稳定、发力更集中。")
+ in 30..49 -> overallEvaluationBuilder.append("本次引体向上需要更多练习。动作中存在一些明显的姿态问题,这会影响训练效果和安全性。")
+ else -> overallEvaluationBuilder.append("引体向上动作仍需大量改进。请务必仔细对照标准,从基础开始练习,避免受伤。")
+ }
+ } else {
+ overallEvaluationBuilder.append("本次引体向上分析完成!发现了一些可以改进的地方:\n")
+ overallEvaluationBuilder.append(errors.joinToString("\n"))
+ overallEvaluationBuilder.append("\n总次数:$repCount")
+ }
+
+ overallEvaluationBuilder.append("\n\n以下是本次训练的详细分析过程,希望能帮助你更好地理解和改进:\n")
+ overallEvaluationBuilder.append(uniqueMessages)
+
+ return overallEvaluationBuilder.toString()
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/evaluator/PushUpEvaluator.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/evaluator/PushUpEvaluator.kt
new file mode 100644
index 0000000..c30510d
--- /dev/null
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/evaluator/PushUpEvaluator.kt
@@ -0,0 +1,237 @@
+package org.tensorflow.lite.examples.poseestimation.evaluator
+
+import org.tensorflow.lite.examples.poseestimation.data.KeyPoint
+import org.tensorflow.lite.examples.poseestimation.data.BodyPart
+import kotlin.math.abs
+import kotlin.math.roundToInt
+
+// 定义俯卧撑动作的阶段
+enum class PushUpState {
+ START, // 起始姿态(手臂伸直)
+ DESCENT, // 下降过程
+ BOTTOM, // 底部姿态(胸部接近地面)
+ ASCENT, // 上推过程
+ LOCKOUT // 恢复到起始姿态
+}
+
+class PushUpEvaluator : ExerciseEvaluator {
+
+ private var currentState: PushUpState = PushUpState.START
+ private var totalScore: Float = 0f
+ private var frameCount: Int = 0
+ private var evaluationMessages: MutableList = mutableListOf()
+ private var repCount: Int = 0
+ private var isRepCounting: Boolean = false
+
+ // 俯卧撑关键角度阈值 (示例值,需要根据实际模型和标准进行调整)
+ private val ELBOW_ANGLE_START_MIN = 170f // 起始姿态(手臂伸直,放宽要求)
+ private val ELBOW_ANGLE_BOTTOM_MAX = 100f // 底部姿态(手肘弯曲程度,放宽要求)
+ private val HIP_SHOULDER_ANKLE_MIN = 160f // 身体直线(髋-肩-踝)
+ private val HIP_SHOULDER_ANKLE_MAX = 185f // 允许略微的弧度
+
+ // Helper function to calculate angle between three keypoints (A-B-C)
+ // B is the vertex of the angle
+ private fun calculateAngle(A: KeyPoint, B: KeyPoint, C: KeyPoint): Float {
+ return B.abttPoints(A, B, C)
+ }
+
+ override fun evaluateFrame(keyPoints: List) {
+ if (keyPoints.isEmpty()) {
+ evaluationMessages.add(EvaluationMessage("未检测到关键点,无法评估。", true))
+ return
+ }
+
+ frameCount++
+
+ // 获取俯卧撑所需关键点
+ val leftShoulder = keyPoints.find { it.bodyPart == BodyPart.LEFT_SHOULDER }
+ val rightShoulder = keyPoints.find { it.bodyPart == BodyPart.RIGHT_SHOULDER }
+ val leftElbow = keyPoints.find { it.bodyPart == BodyPart.LEFT_ELBOW }
+ val rightElbow = keyPoints.find { it.bodyPart == BodyPart.RIGHT_ELBOW }
+ val leftWrist = keyPoints.find { it.bodyPart == BodyPart.LEFT_WRIST }
+ val rightWrist = keyPoints.find { it.bodyPart == BodyPart.RIGHT_WRIST }
+ val leftHip = keyPoints.find { it.bodyPart == BodyPart.LEFT_HIP }
+ val rightHip = keyPoints.find { it.bodyPart == BodyPart.RIGHT_HIP }
+ val leftAnkle = keyPoints.find { it.bodyPart == BodyPart.LEFT_ANKLE }
+ val rightAnkle = keyPoints.find { it.bodyPart == BodyPart.RIGHT_ANKLE }
+
+ // 确保所有关键点都存在
+ if (leftShoulder == null || rightShoulder == null || leftElbow == null || rightElbow == null ||
+ leftWrist == null || rightWrist == null || leftHip == null || rightHip == null ||
+ leftAnkle == null || rightAnkle == null) {
+ evaluationMessages.add(EvaluationMessage("关键点缺失,评估可能不准确。", true))
+ return
+ }
+
+ // 计算左右平均关键点,提高稳定性
+ val midShoulder = KeyPoint(
+ BodyPart.NOSE,
+ android.graphics.PointF(
+ (leftShoulder.coordinate.x + rightShoulder.coordinate.x) / 2,
+ (leftShoulder.coordinate.y + rightShoulder.coordinate.y) / 2
+ ),
+ (leftShoulder.score + rightShoulder.score) / 2
+ )
+ val midElbow = KeyPoint(
+ BodyPart.NOSE,
+ android.graphics.PointF(
+ (leftElbow.coordinate.x + rightElbow.coordinate.x) / 2,
+ (leftElbow.coordinate.y + rightElbow.coordinate.y) / 2
+ ),
+ (leftElbow.score + rightElbow.score) / 2
+ )
+ val midWrist = KeyPoint(
+ BodyPart.NOSE,
+ android.graphics.PointF(
+ (leftWrist.coordinate.x + rightWrist.coordinate.x) / 2,
+ (leftWrist.coordinate.y + rightWrist.coordinate.y) / 2
+ ),
+ (leftWrist.score + rightWrist.score) / 2
+ )
+ val midHip = KeyPoint(
+ BodyPart.NOSE,
+ android.graphics.PointF(
+ (leftHip.coordinate.x + rightHip.coordinate.x) / 2,
+ (leftHip.coordinate.y + rightHip.coordinate.y) / 2
+ ),
+ (leftHip.score + rightHip.score) / 2
+ )
+ val midAnkle = KeyPoint(
+ BodyPart.NOSE,
+ android.graphics.PointF(
+ (leftAnkle.coordinate.x + rightAnkle.coordinate.x) / 2,
+ (leftAnkle.coordinate.y + rightAnkle.coordinate.y) / 2
+ ),
+ (leftAnkle.score + rightAnkle.score) / 2
+ )
+
+ // 计算核心角度
+ val elbowAngle = calculateAngle(midShoulder, midElbow, midWrist) // 肩-肘-腕
+ val bodyLineAngle = calculateAngle(midShoulder, midHip, midAnkle) // 肩-髋-踝
+
+ var frameScore = 0f
+ var frameEvaluation = ""
+ var isBodyStraight = true
+
+ // 检查身体是否呈直线
+ if (bodyLineAngle !in HIP_SHOULDER_ANKLE_MIN..HIP_SHOULDER_ANKLE_MAX) {
+ isBodyStraight = false
+ if (midHip.coordinate.y < midShoulder.coordinate.y) { // 臀部过高
+ evaluationMessages.add(EvaluationMessage("臀部抬得太高了,尝试放低臀部,保持身体呈一条直线。", true))
+ } else if (midHip.coordinate.y > midAnkle.coordinate.y) { // 臀部下沉
+ evaluationMessages.add(EvaluationMessage("臀部下沉了,收紧核心,将臀部向上抬起,保持身体的平直。", true))
+ } else {
+ evaluationMessages.add(EvaluationMessage("身体不够平直,请调整髋部位置。", true))
+ }
+ }
+
+ when (currentState) {
+ PushUpState.START -> {
+ // 评估起始姿态 (手臂伸直,身体呈直线)
+ if (elbowAngle > ELBOW_ANGLE_START_MIN - 10 && isBodyStraight) { // 稍微放宽手臂伸直要求
+ frameEvaluation = "起始姿态良好,手臂基本伸直,身体呈一条直线。"
+ isRepCounting = false
+ currentState = PushUpState.DESCENT
+ frameScore = 200f // 提高分数
+ } else {
+ if (elbowAngle < ELBOW_ANGLE_START_MIN - 10) evaluationMessages.add(EvaluationMessage("手臂未完全伸直,请确保回到起始位置。", true))
+ if (!isBodyStraight) evaluationMessages.add(EvaluationMessage("身体未保持一条直线,请调整核心,不要塌腰或撅臀。", true))
+ frameEvaluation = "请调整至正确起始姿态。"
+ }
+ }
+ PushUpState.DESCENT -> {
+ // 评估下降过程
+ if (elbowAngle < ELBOW_ANGLE_START_MIN - 15) { // 手肘开始弯曲
+ if (isBodyStraight) {
+ frameEvaluation = "下降得很稳,保持身体平直。"
+ currentState = PushUpState.BOTTOM
+ frameScore = 400f // 提高分数
+ } else {
+ evaluationMessages.add(EvaluationMessage("下降时身体不够平直,请调整核心,保持身体直线。", true))
+ frameEvaluation = "下降中,注意保持身体直线。"
+ }
+ } else {
+ frameEvaluation = "继续下降,感受胸部拉伸。"
+ }
+ }
+ PushUpState.BOTTOM -> {
+ // 评估底部姿态 (胸部接近地面,手肘弯曲到位,身体呈直线)
+ if (elbowAngle < ELBOW_ANGLE_BOTTOM_MAX + 10 && isBodyStraight) { // 放宽手肘弯曲
+ frameEvaluation = "底部姿态完美,胸部接近地面!"
+ currentState = PushUpState.ASCENT
+ frameScore = 600f // 提高分数
+ } else {
+ if (elbowAngle > ELBOW_ANGLE_BOTTOM_MAX + 10) evaluationMessages.add(EvaluationMessage("下降不够深,请尝试让胸部更接近地面。", true))
+ if (!isBodyStraight) evaluationMessages.add(EvaluationMessage("底部姿态身体不直,请调整。", true))
+ frameEvaluation = "底部姿态可改进。"
+ }
+ }
+ PushUpState.ASCENT -> {
+ // 评估上推过程
+ if (elbowAngle > ELBOW_ANGLE_BOTTOM_MAX + 15) { // 手肘开始伸直
+ if (isBodyStraight) {
+ frameEvaluation = "上推有力,身体保持平直。"
+ currentState = PushUpState.LOCKOUT
+ frameScore = 400f // 提高分数
+ } else {
+ evaluationMessages.add(EvaluationMessage("上推时身体不够平直,请调整核心,保持身体直线。", true))
+ frameEvaluation = "上推中,注意保持身体直线。"
+ }
+ } else {
+ frameEvaluation = "继续上推,感受胸部和手臂发力。"
+ }
+ }
+ PushUpState.LOCKOUT -> {
+ // 评估完全推起(锁定)姿态
+ if (elbowAngle > ELBOW_ANGLE_START_MIN - 15 && isBodyStraight) { // 稍微放宽手臂伸直要求
+ frameEvaluation = "完美完成一次俯卧撑!手臂基本伸直,身体呈一条直线。"
+ if (!isRepCounting) {
+ repCount++
+ isRepCounting = true
+ evaluationMessages.add(EvaluationMessage("恭喜你,又完成了一次俯卧撑!目前累计完成了 $repCount 次,继续保持!"))
+ }
+ currentState = PushUpState.START // 完成一次后回到起始状态,准备下一次
+ frameScore = 200f // 提高分数
+ } else {
+ if (elbowAngle < ELBOW_ANGLE_START_MIN - 15) evaluationMessages.add(EvaluationMessage("手臂未完全伸直,请确保回到起始位置。", true))
+ if (!isBodyStraight) evaluationMessages.add(EvaluationMessage("锁定姿态身体不直,请调整。", true))
+ frameEvaluation = "请完成锁定:手臂基本伸直,身体保持平直。"
+ }
+ }
+ }
+ totalScore += frameScore
+ evaluationMessages.add(EvaluationMessage(frameEvaluation))
+ }
+
+ override fun getFinalScore(): Float {
+ val rawScore = if (frameCount > 0) totalScore / frameCount else 0f
+ val roundedScore = (rawScore / 10.0f).roundToInt() * 10.0f
+ return roundedScore.coerceIn(0f, 100f)
+ }
+
+ override fun getFinalEvaluation(finalScore: Float): String {
+ val uniqueMessages = evaluationMessages.map { it.message }.distinct().joinToString("\n")
+ val errors = evaluationMessages.filter { it.isError }.map { it.message }.distinct()
+
+ val overallEvaluationBuilder = StringBuilder()
+
+ if (errors.isEmpty()) {
+ when (finalScore.toInt()) {
+ in 90..100 -> overallEvaluationBuilder.append("太棒了!你的俯卧撑动作几乎完美无瑕,姿态标准,力量十足!继续保持!")
+ in 70..89 -> overallEvaluationBuilder.append("非常不错的俯卧撑!动作基本流畅,姿态也比较到位,再稍加注意细节就能更完美!")
+ in 50..69 -> overallEvaluationBuilder.append("俯卧撑动作有进步空间哦!虽然有些地方做得不错,但还需要多练习,让姿态更稳定、发力更集中。")
+ in 30..49 -> overallEvaluationBuilder.append("本次俯卧撑需要更多练习。动作中存在一些明显的姿态问题,这会影响训练效果和安全性。")
+ else -> overallEvaluationBuilder.append("俯卧撑动作仍需大量改进。请务必仔细对照标准,从基础开始练习,避免受伤。")
+ }
+ } else {
+ overallEvaluationBuilder.append("本次俯卧撑分析完成!发现了一些可以改进的地方:\n")
+ overallEvaluationBuilder.append(errors.joinToString("\n"))
+ overallEvaluationBuilder.append("\n总次数:$repCount")
+ }
+
+ overallEvaluationBuilder.append("\n\n以下是本次训练的详细分析过程,希望能帮助你更好地理解和改进:\n")
+ overallEvaluationBuilder.append(uniqueMessages)
+
+ return overallEvaluationBuilder.toString()
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/evaluator/SquatEvaluator.kt b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/evaluator/SquatEvaluator.kt
new file mode 100644
index 0000000..5137b0f
--- /dev/null
+++ b/android/app/src/main/java/org/tensorflow/lite/examples/poseestimation/evaluator/SquatEvaluator.kt
@@ -0,0 +1,218 @@
+package org.tensorflow.lite.examples.poseestimation.evaluator
+
+import org.tensorflow.lite.examples.poseestimation.data.KeyPoint
+import org.tensorflow.lite.examples.poseestimation.data.BodyPart
+import kotlin.math.abs
+import kotlin.math.roundToInt
+
+// 定义深蹲动作的阶段
+enum class SquatState {
+ START, // 起始姿态(站立)
+ DESCENT, // 下蹲过程
+ BOTTOM, // 底部姿态(深蹲最低点)
+ ASCENT, // 起身过程
+ LOCKOUT // 锁定(完成站立)姿态
+}
+
+class SquatEvaluator : ExerciseEvaluator {
+
+ private var currentState: SquatState = SquatState.START
+ private var totalScore: Float = 0f
+ private var frameCount: Int = 0
+ private var evaluationMessages: MutableList = mutableListOf()
+ private var repCount: Int = 0
+ private var isRepCounting: Boolean = false
+
+ // 深蹲关键角度阈值 (示例值,需要根据实际模型和标准进行调整)
+ private val HIP_ANGLE_START_MAX = 175f // 站立时髋关节角度上限
+ private val KNEE_ANGLE_START_MAX = 175f // 站立时膝关节角度上限
+ private val TORSO_ANGLE_START_MAX = 15f // 站立时躯干与垂直方向最大夹角(微前倾)
+
+ private val HIP_ANGLE_BOTTOM_MIN = 60f // 深蹲底部髋关节最小角度(深蹲深度)
+ private val KNEE_ANGLE_BOTTOM_MIN = 70f // 深蹲底部膝关节最小角度(深蹲深度)
+ private val TORSO_ANGLE_BOTTOM_MAX = 45f // 深蹲底部躯干与垂直方向最大夹角(通常会前倾)
+ private val BACK_STRAIGHT_THRESHOLD = 160f // 躯干挺直角度阈值 (肩-髋-膝角度,用于判断背部是否弓起)
+
+ // Helper function to calculate angle between three keypoints (A-B-C)
+ // B is the vertex of the angle
+ private fun calculateAngle(A: KeyPoint, B: KeyPoint, C: KeyPoint): Float {
+ return B.abttPoints(A, B, C)
+ }
+
+ override fun evaluateFrame(keyPoints: List) {
+ if (keyPoints.isEmpty()) {
+ evaluationMessages.add(EvaluationMessage("未检测到关键点,无法评估。", true))
+ return
+ }
+
+ frameCount++
+
+ // 获取深蹲所需关键点
+ val leftShoulder = keyPoints.find { it.bodyPart == BodyPart.LEFT_SHOULDER }
+ val rightShoulder = keyPoints.find { it.bodyPart == BodyPart.RIGHT_SHOULDER }
+ val leftHip = keyPoints.find { it.bodyPart == BodyPart.LEFT_HIP }
+ val rightHip = keyPoints.find { it.bodyPart == BodyPart.RIGHT_HIP }
+ val leftKnee = keyPoints.find { it.bodyPart == BodyPart.LEFT_KNEE }
+ val rightKnee = keyPoints.find { it.bodyPart == BodyPart.RIGHT_KNEE }
+ val leftAnkle = keyPoints.find { it.bodyPart == BodyPart.LEFT_ANKLE }
+ val rightAnkle = keyPoints.find { it.bodyPart == BodyPart.RIGHT_ANKLE }
+
+ // 确保所有关键点都存在,如果缺失,则跳过此帧或给出警告
+ if (leftShoulder == null || rightShoulder == null || leftHip == null || rightHip == null ||
+ leftKnee == null || rightKnee == null || leftAnkle == null || rightAnkle == null) {
+ evaluationMessages.add(EvaluationMessage("关键点缺失,评估可能不准确。", true))
+ return
+ }
+
+ // 计算左右平均关键点,提高稳定性
+ val midShoulder = KeyPoint(
+ BodyPart.NOSE, // 使用 NOSE 作为占位符
+ android.graphics.PointF(
+ (leftShoulder.coordinate.x + rightShoulder.coordinate.x) / 2,
+ (leftShoulder.coordinate.y + rightShoulder.coordinate.y) / 2
+ ),
+ (leftShoulder.score + rightShoulder.score) / 2
+ )
+ val midHip = KeyPoint(
+ BodyPart.NOSE, // 同样,使用 NOSE 作为占位符
+ android.graphics.PointF(
+ (leftHip.coordinate.x + rightHip.coordinate.x) / 2,
+ (leftHip.coordinate.y + rightHip.coordinate.y) / 2
+ ),
+ (leftHip.score + rightHip.score) / 2
+ )
+ val midKnee = KeyPoint(
+ BodyPart.NOSE, // 同样,使用 NOSE 作为占位符
+ android.graphics.PointF(
+ (leftKnee.coordinate.x + rightKnee.coordinate.x) / 2,
+ (leftKnee.coordinate.y + rightKnee.coordinate.y) / 2
+ ),
+ (leftKnee.score + rightKnee.score) / 2
+ )
+ val midAnkle = KeyPoint(
+ BodyPart.NOSE, // 同样,使用 NOSE 作为占位符
+ android.graphics.PointF(
+ (leftAnkle.coordinate.x + rightAnkle.coordinate.x) / 2,
+ (leftAnkle.coordinate.y + rightAnkle.coordinate.y) / 2
+ ),
+ (leftAnkle.score + rightAnkle.score) / 2
+ )
+
+ // 计算核心角度
+ val hipAngle = calculateAngle(midShoulder, midHip, midKnee) // 肩-髋-膝
+ val kneeAngle = calculateAngle(midHip, midKnee, midAnkle) // 髋-膝-踝
+
+ // 躯干角度:通过肩和髋的y坐标差与x坐标差的反正切,并转换为相对于垂直线的角度
+ val dxTorso = midHip.coordinate.x - midShoulder.coordinate.x
+ val dyTorso = midHip.coordinate.y - midShoulder.coordinate.y
+ val torsoAngleVertical = Math.toDegrees(kotlin.math.atan2(dxTorso.toDouble(), dyTorso.toDouble())).toFloat()
+ val realTorsoAngle = abs(torsoAngleVertical) // 简化处理,取绝对值
+
+ var frameScore = 0f
+ var frameEvaluation = ""
+
+ when (currentState) {
+ SquatState.START -> {
+ if (hipAngle > HIP_ANGLE_START_MAX - 5 && kneeAngle > KNEE_ANGLE_START_MAX - 5 && realTorsoAngle < TORSO_ANGLE_START_MAX + 5) {
+ frameEvaluation = "起始姿态良好,准备下蹲。"
+ isRepCounting = false
+ currentState = SquatState.DESCENT
+ frameScore = 300f
+ } else {
+ frameEvaluation = "请调整至正确起始姿态:站直,核心收紧。"
+ evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
+ }
+ }
+ SquatState.DESCENT -> {
+ if (hipAngle < HIP_ANGLE_START_MAX && kneeAngle < KNEE_ANGLE_START_MAX) {
+ if (realTorsoAngle < TORSO_ANGLE_BOTTOM_MAX + 15) { // 下降过程中背部不应过度前倾
+ frameEvaluation = "下降得很稳,保持背部挺直。"
+ currentState = SquatState.BOTTOM
+ frameScore = 300f
+ } else {
+ frameEvaluation = "下蹲时背部有点弓起或过度前倾。"
+ evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
+ }
+ } else {
+ frameEvaluation = "继续下蹲,感受臀腿发力。"
+ }
+ }
+ SquatState.BOTTOM -> {
+ if (hipAngle >= HIP_ANGLE_BOTTOM_MIN && kneeAngle >= KNEE_ANGLE_BOTTOM_MIN && realTorsoAngle <= TORSO_ANGLE_BOTTOM_MAX) {
+ frameEvaluation = "底部姿态非常棒,深蹲深度足够!"
+ currentState = SquatState.ASCENT
+ frameScore = 450f
+ } else {
+ frameEvaluation = "底部姿态可改进。"
+ if (hipAngle < HIP_ANGLE_BOTTOM_MIN) evaluationMessages.add(EvaluationMessage("深蹲深度不足,请尝试蹲得更深。", true))
+ if (kneeAngle < KNEE_ANGLE_BOTTOM_MIN) evaluationMessages.add(EvaluationMessage("膝盖弯曲不足。", true))
+ if (realTorsoAngle > TORSO_ANGLE_BOTTOM_MAX) evaluationMessages.add(EvaluationMessage("背部过度前倾或弓起。", true))
+ }
+ }
+ SquatState.ASCENT -> {
+ if (hipAngle > HIP_ANGLE_BOTTOM_MIN && kneeAngle > KNEE_ANGLE_BOTTOM_MIN) {
+ if (realTorsoAngle < TORSO_ANGLE_START_MAX + 15) { // 上升过程中背部不应过度前倾
+ frameEvaluation = "起身有力,保持背部挺直。"
+ currentState = SquatState.LOCKOUT
+ frameScore = 300f
+ } else {
+ frameEvaluation = "起身时背部有点弓起或过度前倾。"
+ evaluationMessages.add(EvaluationMessage(frameEvaluation, true))
+ }
+ } else {
+ frameEvaluation = "继续起身,将身体推回起始位置。"
+ }
+ }
+ SquatState.LOCKOUT -> {
+ if (hipAngle > HIP_ANGLE_START_MAX - 5 && kneeAngle > KNEE_ANGLE_START_MAX - 5 && realTorsoAngle < TORSO_ANGLE_START_MAX + 5) {
+ frameEvaluation = "完美完成一次深蹲!姿态非常棒!"
+ if (!isRepCounting) {
+ repCount++
+ isRepCounting = true
+ evaluationMessages.add(EvaluationMessage("恭喜你,又完成了一次深蹲!目前累计完成了 $repCount 次,继续保持!"))
+ }
+ currentState = SquatState.START
+ frameScore = 150f
+ } else {
+ frameEvaluation = "请完成锁定:髋部和膝盖完全伸展,全身收紧。"
+ if (hipAngle < HIP_ANGLE_START_MAX - 5 || kneeAngle < KNEE_ANGLE_START_MAX - 5) evaluationMessages.add(EvaluationMessage("顶部锁定还不够充分哦,髋部和膝盖可以再伸展一点。", true))
+ if (realTorsoAngle > TORSO_ANGLE_START_MAX + 5) evaluationMessages.add(EvaluationMessage("注意!在锁定阶段,身体可能有点过度后仰。保持核心收紧,稳稳地完成动作。", true))
+ }
+ }
+ }
+ totalScore += frameScore
+ evaluationMessages.add(EvaluationMessage(frameEvaluation))
+ }
+
+ override fun getFinalScore(): Float {
+ val rawScore = if (frameCount > 0) totalScore / frameCount else 0f
+ val roundedScore = (rawScore / 10.0f).roundToInt() * 10.0f
+ return roundedScore.coerceIn(0f, 100f)
+ }
+
+ override fun getFinalEvaluation(finalScore: Float): String {
+ val uniqueMessages = evaluationMessages.map { it.message }.distinct().joinToString("\n")
+ val errors = evaluationMessages.filter { it.isError }.map { it.message }.distinct()
+
+ val overallEvaluationBuilder = StringBuilder()
+
+ if (errors.isEmpty()) {
+ when (finalScore.toInt()) {
+ in 90..100 -> overallEvaluationBuilder.append("太棒了!你的深蹲动作几乎完美无瑕,姿态标准,力量十足!继续保持!")
+ in 70..89 -> overallEvaluationBuilder.append("非常不错的深蹲!动作基本流畅,姿态也比较到位,再稍加注意细节就能更完美!")
+ in 50..69 -> overallEvaluationBuilder.append("深蹲动作有进步空间哦!虽然有些地方做得不错,但还需要多练习,让姿态更稳定、发力更集中。")
+ in 30..49 -> overallEvaluationBuilder.append("本次深蹲需要更多练习。动作中存在一些明显的姿态问题,这会影响训练效果和安全性。")
+ else -> overallEvaluationBuilder.append("深蹲动作仍需大量改进。请务必仔细对照标准,从基础开始练习,避免受伤。")
+ }
+ } else {
+ overallEvaluationBuilder.append("本次深蹲分析完成!发现了一些可以改进的地方:\n")
+ overallEvaluationBuilder.append(errors.joinToString("\n"))
+ overallEvaluationBuilder.append("\n总次数:$repCount")
+ }
+
+ overallEvaluationBuilder.append("\n\n以下是本次训练的详细分析过程,希望能帮助你更好地理解和改进:\n")
+ overallEvaluationBuilder.append(uniqueMessages)
+
+ return overallEvaluationBuilder.toString()
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/activity_edit_profile.xml b/android/app/src/main/res/layout/activity_edit_profile.xml
index 6710178..9a78fe3 100644
--- a/android/app/src/main/res/layout/activity_edit_profile.xml
+++ b/android/app/src/main/res/layout/activity_edit_profile.xml
@@ -119,6 +119,113 @@
android:layout_marginTop="8dp"
android:background="#333" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/activity_main_tab.xml b/android/app/src/main/res/layout/activity_main_tab.xml
index da339d4..f13cff6 100644
--- a/android/app/src/main/res/layout/activity_main_tab.xml
+++ b/android/app/src/main/res/layout/activity_main_tab.xml
@@ -3,13 +3,40 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".MainTabActivity">
+
+
+
+
+
+
+
+
+
+
+ android:layout_marginBottom="56dp"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/activity_video_analysis_result.xml b/android/app/src/main/res/layout/activity_video_analysis_result.xml
new file mode 100644
index 0000000..2a36968
--- /dev/null
+++ b/android/app/src/main/res/layout/activity_video_analysis_result.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/fragment_data.xml b/android/app/src/main/res/layout/fragment_data.xml
index 2223df6..f7ca807 100644
--- a/android/app/src/main/res/layout/fragment_data.xml
+++ b/android/app/src/main/res/layout/fragment_data.xml
@@ -1,14 +1,22 @@
-
+ android:background="#1C1C1E"
+ tools:context=".DataFragment">
-
-
\ No newline at end of file
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/fragment_setting.xml b/android/app/src/main/res/layout/fragment_setting.xml
index 1603722..59e9bed 100644
--- a/android/app/src/main/res/layout/fragment_setting.xml
+++ b/android/app/src/main/res/layout/fragment_setting.xml
@@ -82,6 +82,29 @@
+
+
+
+
+
+
+
+
+
+ android:orientation="vertical"
+ android:background="#333333">
diff --git a/android/app/src/main/res/layout/item_video_analysis_card.xml b/android/app/src/main/res/layout/item_video_analysis_card.xml
new file mode 100644
index 0000000..ee95be1
--- /dev/null
+++ b/android/app/src/main/res/layout/item_video_analysis_card.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..be4b23f
--- /dev/null
+++ b/android/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file