Commit 5bc4a8c6 authored by 姜天宇's avatar 姜天宇

feat: 特征表增加字段记录图片地址,数据库版本升级1=>2; 增加本地导入导出功能

parent f43bd978
......@@ -9,10 +9,10 @@ android {
defaultConfig {
applicationId "com.wmdigit.cateringdetect"
minSdk 24
minSdk 26
targetSdk 33
versionCode 1000200
versionName "v1.0.2"
versionCode 1000201
versionName "v1.0.2.1"
ndk {
abiFilters 'armeabi-v7a'
......@@ -59,8 +59,8 @@ android {
//如果合并不能解决问题就选择其中一个
merge 'META-INF/proguard/androidx-annotations.pro'
merge 'META-INF/proguard/coroutines.pro'
merge 'lib/arm64-v8a/libc++_shared.so'
merge 'lib/armeabi-v7a/libc++_shared.so'
// merge 'lib/arm64-v8a/libc++_shared.so'
// merge 'lib/armeabi-v7a/libc++_shared.so'
pickFirst 'lib/x86/libc++_shared.so'
pickFirst 'lib/x86_64/libc++_shared.so'
pickFirst 'lib/arm64-v8a/libc++_shared.so'
......
package com.wmdigit.common.base.mvvm;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
......@@ -63,10 +64,28 @@ public abstract class BaseMvvmFragment<VM extends BaseViewModel, DB extends View
protected abstract int getLayoutId();
/**
* 注册观察对象
* 初始化观察者
*
* 本方法用于设置视图模型中数据的观察者,以更新UI组件
* 它观察视图模型的toast消息和加载进度文本,并相应地更新UI
*/
protected void initObserve(){
// 观察toast消息,当消息变化时,调用Toaster的show方法显示toast
mViewModel.toastMessage.observe(requireActivity(), Toaster::show);
// 观察加载进度文本,根据文本的变化来显示或隐藏进度对话框
mViewModel.loadingProgressText.observe(requireActivity(), text -> {
if (TextUtils.isEmpty(text)){
// 当加载进度文本为空时,重置进度对话框的标题并隐藏对话框
mProgressDialog.setTitle("");
mProgressDialog.dismiss();
}
else{
// 当加载进度文本不为空时,更新进度对话框的标题并显示对话框
mProgressDialog.setTitle(text);
mProgressDialog.show();
}
});
}
/**
......
......@@ -25,6 +25,8 @@ public class BaseViewModel extends AndroidViewModel implements DefaultLifecycleO
*/
public SingleLiveEvent<String> toastMessage = new SingleLiveEvent<>();
public SingleLiveEvent<String> loadingProgressText = new SingleLiveEvent<>();
public BaseViewModel(@NonNull Application application) {
super(application);
}
......@@ -75,4 +77,22 @@ public class BaseViewModel extends AndroidViewModel implements DefaultLifecycleO
public void showToast(String message){
toastMessage.postValue(message);
}
/**
* 显示加载中的进度信息
*
* @param text 进度信息文本,用于显示在加载进度框中
*/
public void showLoadingProgress(String text){
loadingProgressText.postValue(text);
}
/**
* 关闭加载中的进度信息
*
* 通过发送空字符串作为进度信息来实现关闭加载进度框的效果
*/
public void closeLoadingProgress(){
loadingProgressText.postValue("");
}
}
This diff is collapsed.
......@@ -78,6 +78,7 @@
<string name="unknown_product">未知商品</string>
<string name="save_success">保存成功</string>
<string name="save_failed">保存失败</string>
<string name="title_download_remote_tool">下载远程工具</string>
<string name="confirm_download_remote_tool">请确认是否下载远程工具</string>
......
......@@ -23,6 +23,7 @@ import io.reactivex.schedulers.Schedulers;
*/
public class HnswRepository {
// 单例模式实现,保证全局只有一个实例
private static HnswRepository instance;
public static HnswRepository getInstance(){
if (instance == null){
......@@ -34,17 +35,17 @@ public class HnswRepository {
}
return instance;
}
/**
* 索引算法
*/
// 核心索引算法实例
private Hnsw hnsw;
// 用于管理订阅,以便于统一取消
private CompositeDisposable compositeDisposable;
/**
* 记录初始化完成情况
*/
// 记录初始化完成情况
private boolean initComplete = false;
// 初始化监听器
private OnHnswInitListener listener;
// 默认特征维度
private int dimension = 128;
public HnswRepository() {
......@@ -53,32 +54,61 @@ public class HnswRepository {
}
/**
* 初始化
* 初始化索引库
* @param dimension 特征维度
*/
public void init(int dimension){
this.dimension = dimension;
initComplete = false;
hnsw.initHnsw(dimension);
Disposable disposable = Observable.create(emitter -> {
// 遍历特征库
queryAndWriteFeaturesIntoHnsw();
emitter.onNext(1);
})
compositeDisposable.add(startHnswDataInitialization());
}
/**
* 启动Hnsw数据初始化
* 该方法使用RxJava在IO线程中初始化Hnsw数据,并在主线程中处理初始化完成后的逻辑
*
* @return Disposable 用于订阅的Disposable对象,可通过它来取消订阅
*/
private Disposable startHnswDataInitialization(){
return initializeHnswData()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(o -> {
initComplete = true;
// 如果存在监听器,则调用监听器的初始化成功方法
if (listener != null){
listener.onInitSuccess();
}
});
compositeDisposable.add(disposable);
}
/**
* 根据特征检索
* @param feature
* @return
* 初始化Hnsw数据结构
*
* 本方法使用Observable模式异步执行Hnsw数据结构的初始化过程包括删除所有现有数据、重新初始化数据结构,
* 以及将特征数据写入新的数据结构中这个方法被设计为异步执行,以便在不阻塞主线程的情况下完成可能耗时的初始化操作
*
* @return Observable<Boolean> 返回一个Observable对象,用于订阅初始化完成的事件
*/
public Observable<Boolean> initializeHnswData(){
return Observable.create(emitter -> {
// 标记初始化未完成
initComplete = false;
// 清空Hnsw图中的所有数据,为重新初始化做准备
hnsw.deleteAll();
// 初始化Hnsw图的数据结构,dimension表示数据的维度
hnsw.initHnsw(dimension);
// 遍历特征库,将特征数据写入Hnsw图中
queryAndWriteFeaturesIntoHnsw();
// 标记初始化完成
initComplete = true;
// 发送信号,表示初始化过程中的一个步骤已完成
emitter.onNext(true);
});
}
/**
* 根据特征检索商品
* @param feature 查询用的特征数组
* @return 返回匹配的商品信息,如果没有找到则返回null
*/
public ProductsPO retrieveByFeature(float[] feature){
if (!initComplete){
......@@ -95,9 +125,9 @@ public class HnswRepository {
/**
* 检索是否存在相同特征
* @param productCode
* @param feature
* @return
* @param productCode 商品代码
* @param feature 特征数组
* @return 如果存在相同特征返回true,否则返回false
*/
public boolean retrieveWhetherExistSameFeature(String productCode, float[] feature){
if (!initComplete){
......@@ -116,9 +146,9 @@ public class HnswRepository {
/**
* 插入索引,并删除超出表上限的特征
* @param id
* @param code
* @param feature
* @param id 特征ID
* @param code 商品代码
* @param feature 特征数组
*/
public void writeFeatureIntoHnsw(long id, String code, float[] feature){
long deleteId = hnsw.writeFeaturesIntoHnsw(id, code, feature);
......@@ -128,8 +158,7 @@ public class HnswRepository {
}
/**
* 查询并写入特征
* @param dimension
* 查询并写入特征到索引库
*/
private void queryAndWriteFeaturesIntoHnsw(){
int page = 1;
......@@ -156,6 +185,9 @@ public class HnswRepository {
}
}
/**
* 删除所有样本,并重新初始化索引库
*/
public void deleteAllSample(){
if (!initComplete){
XLog.i("索引库未完成初始化");
......@@ -166,6 +198,10 @@ public class HnswRepository {
hnsw.initHnsw(dimension);
}
/**
* 设置初始化监听器
* @param listener 初始化监听器实例
*/
public void setListener(OnHnswInitListener listener) {
this.listener = listener;
if (initComplete && listener != null){
......@@ -173,16 +209,28 @@ public class HnswRepository {
}
}
/**
* 检查是否初始化完成
* @return 如果初始化完成返回true,否则返回false
*/
public boolean isInitComplete() {
return initComplete;
}
/**
* 清理资源,包括取消所有订阅和设置监听器为null
*/
public void close(){
compositeDisposable.clear();
listener = null;
}
/**
* 初始化监听器接口,用于通知初始化成功
*/
public static interface OnHnswInitListener{
void onInitSuccess();
}
}
......@@ -7,7 +7,7 @@ android {
compileSdk 33
defaultConfig {
minSdk 24
minSdk 26
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
......
......@@ -21,7 +21,7 @@ public class LocalDataModule {
// 初始化mmkv
MMKV.initialize(context);
// 清空本地识别记录图片
DiskRepository.getInstance().clearIdentifyRecords();
DiskRepository.getInstance().cleanIdentifyRecords();
}
}
......
package com.wmdigit.data.database;
import android.content.Context;
import android.os.Build;
import android.os.Environment;
import androidx.annotation.NonNull;
import androidx.room.Database;
......@@ -8,8 +10,12 @@ import androidx.room.DatabaseConfiguration;
import androidx.room.InvalidationTracker;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import com.elvishew.xlog.XLog;
import com.wmdigit.common.utils.FileUtils;
import com.wmdigit.data.database.dao.FeaturesDao;
import com.wmdigit.data.database.dao.IngredientDao;
import com.wmdigit.data.database.dao.ProductsDao;
......@@ -21,11 +27,14 @@ import com.wmdigit.data.database.entity.ProductsPO;
import com.wmdigit.data.database.entity.SetMenu;
import com.wmdigit.data.database.entity.SetMenuAndIngredient;
import java.io.File;
import java.nio.file.Files;
/**
* @author dizi
*/
@Database(entities = { ProductsPO.class, FeaturesPO.class },
version = 1,
version = 2,
exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
/**
......@@ -35,15 +44,18 @@ public abstract class AppDatabase extends RoomDatabase {
private static volatile AppDatabase instance;
private static Context mContext;
public static AppDatabase getInstance(Context context) {
if (instance == null){
synchronized (AppDatabase.class){
if (instance == null){
mContext = context;
instance = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
.addMigrations(MIGRATION_1_2)
.fallbackToDestructiveMigrationOnDowngrade()
.allowMainThreadQueries()
.setJournalMode(JournalMode.TRUNCATE)
// .createFromAsset("CateringDatabase.db")
.build();
}
}
......@@ -101,4 +113,44 @@ public abstract class AppDatabase extends RoomDatabase {
protected SupportSQLiteOpenHelper createOpenHelper(@NonNull DatabaseConfiguration databaseConfiguration) {
return null;
}
/**
* 复制数据库文件到外部路径
* @return 0-成功 -1-源文件不存在 -2-其他异常
*/
public int copyDatabaseFileToExternalPath(){
File directory = Environment.getExternalStorageDirectory();
String folderPath = directory.getAbsolutePath() + File.separator + "database";
File folder = new File(folderPath);
// 创建文件夹
if (!folder.exists()){
folder.mkdirs();
}
// 指定源文件
File srcFile = mContext.getDatabasePath(DATABASE_NAME);
if (!srcFile.exists()){
return -1;
}
// 指定目标文件
File dstFile = new File(folderPath + File.separator + DATABASE_NAME);
try {
Files.copy(srcFile.toPath(), dstFile.toPath());
}
catch (Exception e){
XLog.e(e);
return -2;
}
return 0;
}
/**
* 1=>2升级脚本,Features表增加imgPath图片路径字段
*/
private static final Migration MIGRATION_1_2 = new Migration(1, 2){
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE `Features` ADD COLUMN `imgPath` TEXT");
}
};
}
package com.wmdigit.data.database;
import android.content.Context;
import android.os.Environment;
import androidx.annotation.NonNull;
import androidx.room.Database;
import androidx.room.DatabaseConfiguration;
import androidx.room.InvalidationTracker;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import com.elvishew.xlog.XLog;
import com.wmdigit.data.database.dao.ExportFeaturesDao;
import com.wmdigit.data.database.entity.ExportFeaturesPO;
import java.io.File;
/**
* 导出用数据库
*
* @author dizi
*/
@Database(entities = {
ExportFeaturesPO.class
}, version = 1, exportSchema = false)
public abstract class ExportDatabase extends RoomDatabase {
private final static String DATABASE_NAME = "ExportCateringDatabase.db";
private static final String EXPORT_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "database" + File.separator + DATABASE_NAME;
private static volatile ExportDatabase instance;
public static ExportDatabase getInstance(Context context) {
if (instance == null){
synchronized (ExportDatabase.class){
if (instance == null){
instance = Room.databaseBuilder(context.getApplicationContext(), ExportDatabase.class, EXPORT_PATH)
.fallbackToDestructiveMigrationOnDowngrade()
.allowMainThreadQueries()
.setJournalMode(JournalMode.TRUNCATE)
.build();
}
}
}
return instance;
}
public static ExportDatabase getInstance() {
return instance;
}
/**
* 关闭数据库
*/
public void closeDatabase(){
if (instance != null && instance.isOpen()){
XLog.i("ExportDatabase关闭");
instance.getOpenHelper().close();
instance.close();
}
instance = null;
}
@Override
public void clearAllTables() {
}
@NonNull
@Override
protected InvalidationTracker createInvalidationTracker() {
return null;
}
@NonNull
@Override
protected SupportSQLiteOpenHelper createOpenHelper(@NonNull DatabaseConfiguration databaseConfiguration) {
return null;
}
/**
* 获取导出学习数据表的DAO
* @return
*/
public abstract ExportFeaturesDao getExportFeaturesDao();
/**
* 获取导出路径
* @return
*/
public String getExportPath() {
return EXPORT_PATH;
}
}
package com.wmdigit.data.database.dao;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import com.wmdigit.data.database.entity.ExportFeaturesPO;
import java.util.List;
/**
* 导出学习数据表的DAO
* @author dizi
*/
@Dao
public interface ExportFeaturesDao {
/**
* 删除全部数据
*/
@Query("Delete from ExportFeatures")
void deleteAll();
/**
* 插入全部数据
* @param list
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(List<ExportFeaturesPO> list);
/**
* 分页查询
* @param offset
* @param limit
* @return
*/
@Query("Select * from ExportFeatures limit :limit offset :offset ")
List<ExportFeaturesPO> queryByPage(int offset, int limit);
}
......@@ -2,6 +2,7 @@ package com.wmdigit.data.database.dao;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import com.wmdigit.data.database.entity.FeaturesPO;
......@@ -24,6 +25,13 @@ public interface FeaturesDao {
@Query("SELECT * FROM Features LIMIT :offset, :pageSize")
List<FeaturesPO> queryByPage(int offset, int pageSize);
/**
* 查询所有的图片路径
* @return
*/
@Query("Select imgPath From Features")
List<String> queryAllImgPath();
/**
* 根据ID删除
* @param id
......@@ -38,6 +46,12 @@ public interface FeaturesDao {
@Insert
long insert(FeaturesPO featuresPO);
/**
* 批量插入数据
* @param list
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(List<FeaturesPO> list);
/**
* 删除所有特性数据
......@@ -51,4 +65,15 @@ public interface FeaturesDao {
@Query("DELETE FROM Features")
int deleteAll();
/**
* 查询Features表中的记录数
*
* 此方法通过执行SQL查询来统计Features表中的所有记录数
* 使用了注解@Query来定义查询语句,这种方式使得查询逻辑更加清晰和集中
*
* @return Features表中的记录总数
*/
@Query("SELECT COUNT(*) FROM Features")
int getCount();
}
package com.wmdigit.data.database.entity;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.Index;
import androidx.room.PrimaryKey;
/**
* 导出向量表
*/
@Entity(tableName = "ExportFeatures")
public class ExportFeaturesPO {
@PrimaryKey(autoGenerate = true)
private long id;
/**
* 商品CODE
*/
private String productCode;
/**
* 特征
*/
private String feature;
/**
* 图片路径
*/
private String imgPath;
public ExportFeaturesPO() {
}
@Ignore
public ExportFeaturesPO(long id, String productCode, String feature, String imgPath) {
this.id = id;
this.productCode = productCode;
this.feature = feature;
this.imgPath = imgPath;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getProductCode() {
return productCode;
}
public void setProductCode(String productCode) {
this.productCode = productCode;
}
public String getFeature() {
return feature;
}
public String getImgPath() {
return imgPath;
}
public void setImgPath(String imgPath) {
this.imgPath = imgPath;
}
public void setFeature(String feature) {
this.feature = feature;
}
}
......@@ -14,25 +14,28 @@ public class FeaturesPO {
@PrimaryKey(autoGenerate = true)
private long id;
/**
* 商品CODE
*/
private String productCode;
/**
* 特征
*/
private String feature;
/**
* 文件路径
*/
private String imgPath;
public FeaturesPO() {
}
@Ignore
public FeaturesPO(long id, String productCode, String feature) {
public FeaturesPO(long id, String productCode, String feature, String imgPath) {
this.id = id;
this.productCode = productCode;
this.feature = feature;
this.imgPath = imgPath;
}
public long getId() {
......@@ -58,4 +61,12 @@ public class FeaturesPO {
public void setFeature(String feature) {
this.feature = feature;
}
public String getImgPath() {
return imgPath;
}
public void setImgPath(String imgPath) {
this.imgPath = imgPath;
}
}
package com.wmdigit.data.database.mapper;
import com.wmdigit.data.database.entity.ExportFeaturesPO;
import com.wmdigit.data.database.entity.FeaturesPO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* FeaturesMapper接口用于定义特征信息的映射操作
* 它主要负责在不同的数据表示形式之间进行转换,特别是与特征导出相关的持久化对象(PO)
*/
@Mapper
public interface FeaturesMapper {
/**
* INSTANCE是一个静态常量,用于获取FeaturesMapper的实例
* 通过Mappers.getMapper方法初始化,便于在需要的地方直接使用,减少重复代码
*/
FeaturesMapper INSTANCE = Mappers.getMapper(FeaturesMapper.class);
/**
* 将ExportFeaturesPO对象列表转换为另一个ExportFeaturesPO对象列表
* 这个方法的主要作用是进行数据的重新组织或格式转换,以便于特征导出功能的实现
*
* @param poList 一个包含多个FeaturesPO对象的列表,代表待转换的原始数据
* @return 返回一个新的ExportFeaturesPO列表,代表转换后的数据
*/
List<ExportFeaturesPO> toExportFeaturesPOList(List<FeaturesPO> poList);
/**
* 将单个ExportFeaturesPO对象转换为另一个ExportFeaturesPO对象
* 这个方法用于对单个数据对象进行转换或映射,同样是为了适应特征导出功能的需求
*
* @param po 一个FeaturesPO对象,代表待转换的原始数据
* @return 返回一个新的ExportFeaturesPO对象,代表转换后的数据
*/
ExportFeaturesPO toExportFeaturesPO(FeaturesPO po);
/**
* 将ExportFeaturesPO列表转换为FeaturesPO列表
* 此方法用于批量转换对象,提高处理效率
*
* @param list ExportFeaturesPO对象的列表,代表待转换的特征数据
* @return 转换后的FeaturesPO对象列表
*/
List<FeaturesPO> toFeaturesPOList(List<ExportFeaturesPO> list);
}
package com.wmdigit.data.database.repository;
import com.wmdigit.data.LocalDataModule;
import com.wmdigit.data.database.ExportDatabase;
import com.wmdigit.data.database.dao.ExportFeaturesDao;
import com.wmdigit.data.database.entity.ExportFeaturesPO;
import com.wmdigit.data.database.entity.FeaturesPO;
import com.wmdigit.data.database.mapper.FeaturesMapper;
import com.wmdigit.data.disk.repository.DiskRepository;
import java.io.File;
import java.util.List;
import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers;
/**
* @author dizi
*/
public class ExportFeaturesRepository {
private static volatile ExportFeaturesRepository instance;
public static ExportFeaturesRepository getInstance() {
if (instance == null){
synchronized (ExportFeaturesRepository.class){
if (instance == null){
instance = new ExportFeaturesRepository();
}
}
}
return instance;
}
private ExportFeaturesDao getExportFeaturesDao(){
return ExportDatabase.getInstance(LocalDataModule.getAppContext()).getExportFeaturesDao();
}
/**
* 删除全部数据
*/
public void deleteAll(){
getExportFeaturesDao().deleteAll();
}
/**
* 插入列表
* @param list
*/
public void insert(List<ExportFeaturesPO> list){
getExportFeaturesDao().insert(list);
}
/**
* 分页查询
* @param page 页码,从1开始计数
* @param pageSize
* @return
*/
public List<ExportFeaturesPO> queryByPage(int page, int pageSize){
int offset = pageSize * (page - 1);
return getExportFeaturesDao().queryByPage(offset, pageSize);
}
/**
* 异步导出数据库信息和图片
* 该方法将导出操作放在异步线程中执行,以避免阻塞主线程
*
* @param onNext 消费者接口,用于处理导出成功后的操作
* @param onError 消费者接口,用于处理导出过程中发生的错误
* @return Disposable对象,用于管理订阅的生命周期
*/
public Disposable asyncExportLearningData(Consumer<Object> onNext, Consumer<Object> onError){
return Observable.create(emitter -> {
// 删除所有已导出的数据,确保开始新一轮的导出
deleteAll();
// 导出数据库中的数据
exportDatabase();
// 导出图片信息
exportImages();
// 通知观察者导出完成
emitter.onNext(true);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(onNext, onError);
}
/**
* 导出数据库中的数据
* 该方法分页查询数据库中的数据,并将其转换后插入到导出数据库中
*/
private void exportDatabase(){
int page = 1;
int pageSize = 1000;
while (true){
// 分页查询数据库中的数据
List<FeaturesPO> list = FeaturesRepository.getInstance().getFeaturesByPage(page, pageSize);
// 将查询结果转换为导出对象列表
List<ExportFeaturesPO> exportList = FeaturesMapper.INSTANCE.toExportFeaturesPOList(list);
// 将转换后的数据插入到导出数据库中
insert(exportList);
// 如果查询结果小于页面大小,说明所有数据已经查询完毕,退出循环
if (list.size() < pageSize){
break;
}
// 增加页码,继续下一页的查询
page++;
}
// 导出完成后,关闭数据库连接
// ExportDatabase.getInstance().closeDatabase();
}
/**
* 导出图片信息
* 该方法将图片信息打包压缩,以便于导出
*/
private void exportImages(){
// 调用磁盘仓库实例的压缩方法,对识别记录进行压缩
DiskRepository.getInstance().zipIdentifyRecords();
}
/**
* 判断导出文件是否存在
* @return
*/
public boolean isExistExportFile() {
File file = new File(ExportDatabase.getInstance(LocalDataModule.getAppContext()).getExportPath());
return file.exists();
}
/**
* 异步导入学习数据
* 该方法用于在异步环境下导入数据库和图片资源,并进行相应的处理
*
* @param function 用于处理数据的函数
* @param onNext 处理数据成功时的回调
* @param onError 处理数据失败时的回调
* @return Disposable对象,用于管理订阅的生命周期
*/
public Disposable asyncImportLearningData(Function<Object, ObservableSource<?>> function,Consumer<Object> onNext, Consumer<Object> onError){
return Observable.create(emitter -> {
// 导入数据库
importDatabase();
// 解压图片
importImages();
// 通知观察者数据已准备好
emitter.onNext(true);
}).flatMap(function)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(onNext, onError);
}
/**
* 导入数据库方法
* 该方法用于分页查询数据,将查询到的数据转换格式后批量插入数据库
* 直到查询到的数据量小于设定的页面大小,表明已导入所有数据
*/
private void importDatabase(){
// 初始化页码为第1页
int page = 1;
// 设置每页的大小为1000条记录
int pageSize = 1000;
// 无限循环,直到查询到的数据量小于页面大小,表明已导入所有数据
while (true){
// 根据页码和页面大小查询数据
List<ExportFeaturesPO> list = queryByPage(page, pageSize);
// 将查询到的数据转换格式,以便插入数据库
List<FeaturesPO> importList = FeaturesMapper.INSTANCE.toFeaturesPOList(list);
// 批量插入数据到数据库
FeaturesRepository.getInstance().insert(importList);
// 如果查询到的数据量小于页面大小,表明已导入所有数据,退出循环
if (list.size() < pageSize){
break;
}
// 页码递增,准备查询下一页数据
page ++;
}
// 导入完成后,关闭数据库连接
// ExportDatabase.getInstance().closeDatabase();
}
/**
* 导入图片
*/
private void importImages(){
DiskRepository.getInstance().unzipIdentifyRecords();
}
}
......@@ -5,6 +5,7 @@ import android.text.TextUtils;
import com.elvishew.xlog.XLog;
import com.wmdigit.data.database.AppDatabase;
import com.wmdigit.data.database.dao.FeaturesDao;
import com.wmdigit.data.database.entity.ExportFeaturesPO;
import com.wmdigit.data.database.entity.FeaturesPO;
import java.util.Arrays;
......@@ -43,12 +44,21 @@ public class FeaturesRepository {
return getFeaturesDao().queryByPage(offset, pageSize);
}
/**
* 获取所有图片路径
* @return
*/
public List<String> getAllImgPath(){
return getFeaturesDao().queryAllImgPath();
}
/**
* 插入
* @param productCode
* @param feature
* @param imgPath
*/
public long insert(String productCode, float[] feature){
public long insert(String productCode, float[] feature, String imgPath){
if (TextUtils.isEmpty(productCode) || feature == null){
return -1;
}
......@@ -58,9 +68,18 @@ public class FeaturesRepository {
FeaturesPO featuresPO = new FeaturesPO();
featuresPO.setFeature(featureStr);
featuresPO.setProductCode(productCode);
featuresPO.setImgPath(imgPath);
return getFeaturesDao().insert(featuresPO);
}
/**
* 批量插入
* @param list
*/
public void insert(List<FeaturesPO> list){
getFeaturesDao().insert(list);
}
/**
* 根据ID删除
* @param id
......@@ -79,5 +98,17 @@ public class FeaturesRepository {
public void deleteAll(){
int deleteCount = getFeaturesDao().deleteAll();
XLog.i("删除了%s条数据", deleteCount);
};
}
/**
* 获取特征的数量
*
* 此方法通过调用特征数据访问对象(FeaturesDao)的 getCount 方法来获取特征的数量
* 使用这种封装方式,可以使代码结构更清晰,便于维护和测试
*
* @return 特征的数量
*/
public int getCount(){
return getFeaturesDao().getCount();
}
}
......@@ -14,9 +14,11 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/**
* 不再使用
* 套餐、原料相关的Repository
* @author dizi
*/
@Deprecated
public class SetMenuAndIngredientRepository {
private static volatile SetMenuAndIngredientRepository instance;
......
......@@ -2,16 +2,30 @@ package com.wmdigit.data.disk.repository;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Environment;
import android.text.TextUtils;
import com.elvishew.xlog.XLog;
import com.wmdigit.common.utils.BitmapUtils;
import com.wmdigit.common.utils.ZipUtils;
import com.wmdigit.data.LocalDataModule;
import com.wmdigit.data.database.entity.ProductsPO;
import com.wmdigit.data.database.repository.FeaturesRepository;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
/**
* @author dizi
......@@ -45,17 +59,20 @@ public class DiskRepository {
* @param bitmap
* @param rectArray
* @param products
* @return 返回原图的路径
* @return String[] 0-大图路径 1、2、3...n-切图路径
*/
public String saveIdentifyRecordToDisk(Bitmap bitmap, int[][] rectArray, List<ProductsPO> products){
public String[] saveIdentifyRecordToDisk(Bitmap bitmap, int[][] rectArray, List<ProductsPO> products){
List<String> results = new ArrayList<>();
Context context = LocalDataModule.getAppContext();
// 识别记录根目录
String identifyRecordsRootPath = context.getExternalFilesDir(FOLDER_NAME_IDENTIFY_RECORDS).getAbsolutePath();
String imageFilename = UUID.randomUUID().toString() + ".jpg";
// 保存图片
BitmapUtils.saveBitmap(bitmap, identifyRecordsRootPath + "/" + imageFilename);
// 记录原始图片位置
results.add(identifyRecordsRootPath + "/" + imageFilename);
if (rectArray == null || products == null){
return identifyRecordsRootPath + "/" + imageFilename;
return results.toArray(new String[1]);
}
// 商品的学习记录根目录
String productLearningRecordsRootPath = context.getExternalFilesDir(FOLDER_NAME_PRODUCT_LEARNING_RECORDS).getAbsolutePath();
......@@ -66,17 +83,19 @@ public class DiskRepository {
}
Bitmap bitmapTemp = Bitmap.createBitmap(bitmap, rectArray[i][0], rectArray[i][1], rectArray[i][2] - rectArray[i][0], rectArray[i][3] - rectArray[i][1]);
// 保存裁出来的图片
BitmapUtils.saveBitmap(bitmapTemp, productLearningRecordsRootPath + "/" + products.get(i).getProductCode() + "/" + imageFilename.replace(".jpg", "") + "_" + i + ".jpg");
String path = productLearningRecordsRootPath + "/" + products.get(i).getProductCode() + "/" + imageFilename.replace(".jpg", "") + "_" + i + ".jpg";
BitmapUtils.saveBitmap(bitmapTemp, path);
results.add(path);
bitmapTemp.recycle();
}
return identifyRecordsRootPath + "/" + imageFilename;
return results.toArray(new String[results.size()]);
}
/**
* 删除识别记录文件夹图片
*/
public void clearIdentifyRecords(){
public void cleanIdentifyRecords(){
Context context = LocalDataModule.getAppContext();
String identifyRecordsRootPath = context.getExternalFilesDir(FOLDER_NAME_IDENTIFY_RECORDS).getAbsolutePath();
File folder = new File(identifyRecordsRootPath);
......@@ -88,4 +107,84 @@ public class DiskRepository {
}
}
/* public void cleanOrphanedImages(){
String folderPath = LocalDataModule.getAppContext().getExternalFilesDir(FOLDER_NAME_PRODUCT_LEARNING_RECORDS).getAbsolutePath();
File rootImageDir = new File(folderPath);
if (!rootImageDir.exists()){
return;
}
long startTime = System.currentTimeMillis();
Set<String> dbImgPaths = new ConcurrentSkipListSet<>(FeaturesRepository.getInstance().getAllImgPath());
try (Stream<Path> pathStream = Files.walk(rootImageDir.toPath())){
pathStream
.parallel()
.filter(Files::isRegularFile)
.forEach(filePath -> {
String absPath = filePath.toString();
if (!dbImgPaths.contains(absPath)) {
try {
Files.delete(filePath);
} catch (IOException e) {
XLog.e(e);
}
}
});
}
catch (Exception e){
XLog.e(e);
}
}*/
/**
* 将产品的学习记录打包成zip文件
* 此方法首先获取应用上下文,然后确定源文件夹路径和目标zip文件路径
* 如果目标zip文件已存在,则将其删除,以确保生成的是最新记录的zip文件
* 最后,使用ZipUtils工具类将源文件夹压缩成zip文件
*/
public void zipIdentifyRecords(){
// 获取应用上下文
Context context = LocalDataModule.getAppContext();
// 商品的学习记录根目录
String sourcePath = context.getExternalFilesDir(FOLDER_NAME_PRODUCT_LEARNING_RECORDS).getAbsolutePath();
// 目标zip文件路径
String targetPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "database" + File.separator + FOLDER_NAME_PRODUCT_LEARNING_RECORDS + ".zip";
// 创建目标zip文件对象
File targetFile = new File(targetPath);
// 如果目标zip文件已存在,则将其删除
if (targetFile.exists()){
targetFile.delete();
}
// 压缩文件
ZipUtils.zipFile(sourcePath, targetPath);
}
/**
* 解压识别记录文件
* 该方法用于解压存储在外部存储中的识别记录zip文件到指定目录
* 主要目的是为了恢复或导入识别记录数据
*/
public void unzipIdentifyRecords(){
// 获取应用上下文
Context context = LocalDataModule.getAppContext();
// 构造源文件路径,指向外部存储中的识别记录zip文件
String sourcePath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "database" + File.separator + FOLDER_NAME_PRODUCT_LEARNING_RECORDS + ".zip";
// 创建源文件对象
File zipFile = new File(sourcePath);
// 检查源文件是否存在,如果不存在则记录错误日志并返回
if (!zipFile.exists()){
XLog.e("图片压缩包不存在");
return;
}
// 构造目标路径,指向应用外部文件目录中的识别记录文件夹
String targetPath = context.getExternalFilesDir(FOLDER_NAME_PRODUCT_LEARNING_RECORDS).getAbsolutePath();
File targetFile = new File(targetPath);
try {
// 调用ZipUtils工具类的unzip方法解压文件
ZipUtils.unzip(sourcePath, targetFile.getParent());
} catch (Exception e) {
// 捕获并记录解压过程中可能发生的异常
XLog.e(e);
}
}
}
......@@ -10,12 +10,14 @@ v1.0.2 2024/08/06 1.增加系统信息页
7.增加AIDL服务
8.集成标框、菜品识别、餐盘识别算法
9.集成索引库算法(索引库版本较老,可能存在最后一条索引删除不掉的BUG)
10.todo 菜品算法更新128模型
10.菜品算法更新128模型
11.增加数据上传
12.增加【清空商品数据】、【SN解绑】、【下载远程工具】、【检查更新】功能
13.增加摄像头断线重连
14.todo 增加学习记录管理模块
15.todo 替换LOGO
16.todo 增加过期数据清理
17.todo 增加轮询job
18.摄像头改为UvcCamera
\ No newline at end of file
14.摄像头改为UvcCamera
v1.0.2.1 2025/04/22 1.特征表增加字段记录图片地址,数据库版本升级1=>2
2.增加本地导入导出功能
todo 增加学习记录管理模块
todo 替换LOGO
todo 增加轮询job
\ No newline at end of file
......@@ -84,21 +84,6 @@ public class SettingActivity extends BaseMvvmNaviDrawerActivity<SettingViewModel
protected void onStart() {
super.onStart();
// 开相机
/*if(!CameraxController.getInstance(this).isOpen()){
isCameraNeedRelease = true;
// 将相机生命周期和页面绑定,开启相机
if (CameraLocalRepository.getInstance().getCamera().getVid() == 0){
CameraxController.getInstance()
.setLifecycleOwner(this)
.setFrameInterval(2)
.bindCamera();
}
else{
CameraxController.getInstance()
.setLifecycleOwner(this)
.setFrameInterval(2);
}
}*/
if (!USBCameraHelper.getInstance(this).checkCameraOpened()){
isCameraNeedRelease = true;
USBCameraHelper.getInstance(this).open();
......@@ -111,7 +96,6 @@ public class SettingActivity extends BaseMvvmNaviDrawerActivity<SettingViewModel
// 关相机
if (isCameraNeedRelease){
isCameraNeedRelease = false;
// CameraxController.getInstance().destroy();
USBCameraHelper.getInstance(this).destroy();
}
}
......
......@@ -41,6 +41,16 @@ public class DataManagerFragment extends BaseMvvmFragment<DataManagerViewModel,
protected void initListener() {
adapter.setOnItemClickListener(position -> {
switch (position) {
case 0:
// 导出学习数据
exportLearningData();
break;
case 1:
// 导入学习数据
importLearningData();
break;
case 2:
// 清空学习数据
clearLearningData();
......@@ -67,6 +77,31 @@ public class DataManagerFragment extends BaseMvvmFragment<DataManagerViewModel,
});
}
/**
* 导入学习数据
*/
private void importLearningData() {
mSecondConfirmWindow.setTitle(getString(com.wmdigit.common.R.string.prompt))
.setContent(getString(R.string.confirm_import_learning_data))
.setClickListener(() -> {
mViewModel.importLearningData();
})
.showWindow();
}
/**
* 导出学习数据
*/
private void exportLearningData() {
mSecondConfirmWindow.setTitle(getString(com.wmdigit.common.R.string.prompt))
.setContent(getString(R.string.confirm_export_learning_data))
.setClickListener(() -> {
mViewModel.exportLearningData();
})
.showWindow();
}
/**
* 下载远程工具
*
......
......@@ -400,15 +400,19 @@ public class DataLearningViewModel extends BaseViewModel {
*/
public void saveDetectResult(){
synchronized (detectResultLock){
if (detectResult == null || detectResult.getProducts() == null){
if (detectResult == null || detectResult.getProducts() == null || detectResult.getProducts().size() == 0){
return;
}
String[] productNames = new String[detectResult.getProducts().size()];
String[] productCodes = new String[detectResult.getProducts().size()];
// 保存本地,并获取保存的路径
String[] imagePaths = DiskRepository.getInstance().saveIdentifyRecordToDisk(detectResult.getBitmap(), detectResult.getRectArray(), detectResult.getProducts());
// 计数,标识保存成功数量
int learnSuccessCount = 0;
for (int i = 0; i < detectResult.getProducts().size(); i++){
productNames[i] = "";
productCodes[i] = "";
if (TextUtils.isEmpty(detectResult.getProducts().get(i).getProductCode())){
if (detectResult.getProducts().get(i) == null || TextUtils.isEmpty(detectResult.getProducts().get(i).getProductCode())){
continue;
}
if (detectResult.getFeatures()[i] == null){
......@@ -420,20 +424,26 @@ public class DataLearningViewModel extends BaseViewModel {
continue;
}
// 插入特征数据库
long id = FeaturesRepository.getInstance().insert(detectResult.getProducts().get(i).getProductCode(), detectResult.getFeatures()[i]);
long id = FeaturesRepository.getInstance().insert(detectResult.getProducts().get(i).getProductCode(), detectResult.getFeatures()[i], imagePaths[i + 1]);
// 插入索引库
if (id != -1) {
HnswRepository.getInstance().writeFeatureIntoHnsw(id, detectResult.getProducts().get(i).getProductCode(), detectResult.getFeatures()[i]);
}
productNames[i] = detectResult.getProducts().get(i).getProductName();
productCodes[i] = detectResult.getProducts().get(i).getProductCode();
learnSuccessCount ++;
}
String imagePath = DiskRepository.getInstance().saveIdentifyRecordToDisk(detectResult.getBitmap(), detectResult.getRectArray(), detectResult.getProducts());
if (learnSuccessCount == 0){
// 学习失败
toastMessage.postValue(getApplication().getString(com.wmdigit.common.R.string.save_failed));
return;
}
// 学习成功
toastMessage.postValue(getApplication().getString(com.wmdigit.common.R.string.save_success));
// 异步上传后台
IdentifyRecordDTO identifyRecordDTO = new IdentifyRecordDTO(detectResult.getRectArray(), productNames, productCodes);
String json = new Gson().toJson(identifyRecordDTO);
compositeDisposable.add(IdentifyRecordRepository.getInstance().uploadIdentifyRecord(json, imagePath));
compositeDisposable.add(IdentifyRecordRepository.getInstance().uploadIdentifyRecord(json, imagePaths[0]));
}
}
......
......@@ -4,9 +4,11 @@ import android.app.Application;
import androidx.annotation.NonNull;
import com.elvishew.xlog.XLog;
import com.wmdigit.common.base.mvvm.BaseViewModel;
import com.wmdigit.common.base.mvvm.SingleLiveEvent;
import com.wmdigit.core.hnsw.HnswRepository;
import com.wmdigit.data.database.repository.ExportFeaturesRepository;
import com.wmdigit.data.database.repository.FeaturesRepository;
import com.wmdigit.data.database.repository.ProductsRepository;
import com.wmdigit.network.repository.UserRemoteRepository;
......@@ -34,14 +36,83 @@ public class DataManagerViewModel extends BaseViewModel {
private void getData(){
httpToast = UserRemoteRepository.getInstance().mHttpToast;
// 初始化功能键列表
funcButtons.add(new FuncButton(getApplication().getString(R.string.module_setting_upload_database_to_server), R.drawable.ic_upload_learning_data));
funcButtons.add(new FuncButton(getApplication().getString(R.string.module_setting_download_database_from_server), R.drawable.ic_download_learning_data));
funcButtons.add(new FuncButton(getApplication().getString(R.string.export_learning_data), R.drawable.ic_upload_learning_data));
funcButtons.add(new FuncButton(getApplication().getString(R.string.import_learning_data), R.drawable.ic_download_learning_data));
funcButtons.add(new FuncButton(getApplication().getString(R.string.module_setting_clear_learning_data), R.drawable.ic_clear_data_red));
funcButtons.add(new FuncButton(getApplication().getString(R.string.module_setting_clear_products), R.drawable.ic_clean_learning_data));
funcButtons.add(new FuncButton(getApplication().getString(R.string.module_setting_unbind), R.drawable.ic_unbind));
funcButtons.add(new FuncButton(getApplication().getString(R.string.module_setting_download_remote_tool), R.drawable.ic_download_remote));
}
/**
* 导出学习数据
*
* 此方法首先检查是否有数据可供导出如果数据库中没有数据,则显示提示消息并返回
* 如果有数据,将显示加载进度文本,并开始异步导出数据库
* 导出成功后,将更新UI以显示成功消息,否则显示失败消息
*/
public void exportLearningData(){
// 判断是否有数据
if (FeaturesRepository.getInstance().getCount() == 0){
toastMessage.postValue(getApplication().getString(R.string.export_learning_data_no_data));
return;
}
// 准备导出数据,显示加载进度文本
loadingProgressText.postValue(getApplication().getString(R.string.exporting_learning_data));
// 异步导出数据库,根据结果更新UI
compositeDisposable.add(ExportFeaturesRepository.getInstance().asyncExportLearningData(
success->{
// 导出成功后,隐藏加载进度文本并显示成功消息
loadingProgressText.postValue("");
toastMessage.postValue(getApplication().getString(R.string.export_success));
},
error->{
XLog.e(error);
// 导出失败后,隐藏加载进度文本并显示失败消息
loadingProgressText.postValue("");
toastMessage.postValue(getApplication().getString(R.string.export_failed));
})
);
}
/**
* 导入学习数据
* 此方法检查本地是否存在可导入的数据库文件,如果存在,则开始导入学习数据
* 在导入数据期间,会显示加载进度文本,并在成功或失败时更新UI
*/
public void importLearningData(){
// 判断本地是否存在可导入得数据库文件
if (!ExportFeaturesRepository.getInstance().isExistExportFile()){
// 如果不存在,显示提示信息并返回
toastMessage.postValue(getApplication().getString(R.string.import_database_file_not_exits));
return;
}
// 存在可导入的文件时,显示导入学习数据的加载进度文本
loadingProgressText.postValue(getApplication().getString(R.string.importing_learning_data));
// 异步导入学习数据
compositeDisposable.add(ExportFeaturesRepository.getInstance().asyncImportLearningData(
// 导入后的工作,初始化HNWS数据结构
obj-> HnswRepository.getInstance().initializeHnswData(),
// 导入成功时的处理
success->{
// 隐藏加载进度文本
loadingProgressText.postValue("");
// 显示导入成功的提示信息
toastMessage.postValue(getApplication().getString(R.string.import_success));
},
// 导入失败时的处理
error->{
// 记录错误日志
XLog.e(error);
// 隐藏加载进度文本
loadingProgressText.postValue("");
// 显示导入失败的提示信息
toastMessage.postValue(getApplication().getString(R.string.import_failed));
})
);
}
/**
* 清除学习数据
*
......
......@@ -48,4 +48,16 @@
<string name="packaging_remote_tool">正在安装远程工具</string>
<string name="download_fail">下载失败</string>
<string name="download_retry">下载成功</string>
<string name="export_learning_data">导出学习数据</string>
<string name="import_learning_data">导入学习数据</string>
<string name="confirm_export_learning_data">请确认是否导出学习数据?</string>
<string name="confirm_import_learning_data">请确认是否导入学习数据?</string>
<string name="exporting_learning_data">正在导出学习数据,请勿中断操作</string>
<string name="importing_learning_data">正在导入学习数据,请勿中断操作</string>
<string name="export_learning_data_no_data">本地没有学习数据,导出终止</string>
<string name="export_success">学习数据导出成功</string>
<string name="export_failed">学习数据导出失败,请联系运维人员</string>
<string name="import_database_file_not_exits">待导入的数据库文件不存在</string>
<string name="import_success">导入成功</string>
<string name="import_failed">导入失败</string>
</resources>
\ No newline at end of file
......@@ -407,7 +407,7 @@ public class CateringInterfaceImpl extends ICateringInterface.Stub{
String json = new Gson().toJson(identifyRecordDTO);
Bitmap bitmapTemp = ParcelHelper.copy(bitmap);
Disposable disposable = Observable.create(emitter -> {
String filePath = DiskRepository.getInstance().saveIdentifyRecordToDisk(bitmapTemp, null, null);
String filePath = DiskRepository.getInstance().saveIdentifyRecordToDisk(bitmapTemp, null, null)[0];
String url = OSSManager.getInstance().syncUploadFileToOSS(new File(filePath));
emitter.onNext(url);
}).flatMap(url ->{
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment