add open source traffic sign, vehicle and shape-color detector
This commit is contained in:
+5
-1
@@ -49,10 +49,14 @@ dependencies {
|
||||
implementation 'com.google.android.material:material:1.6.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'com.rmtheis:tess-two:9.1.0'
|
||||
implementation files('libs/camerautil.jar')
|
||||
implementation project(path: ':opencv')
|
||||
implementation files('libs/zxingcpp-release.aar')
|
||||
implementation files('libs/Yolov5-tflite-Detector.aar')
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
//开源项目使用的库
|
||||
implementation 'org.tensorflow:tensorflow-lite:2.4.0'
|
||||
implementation 'org.tensorflow:tensorflow-lite-gpu:2.4.0'
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -18,7 +18,6 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
@@ -37,7 +36,8 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.MainCar"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
tools:targetApi="31"
|
||||
tools:replace="android:label">
|
||||
<activity
|
||||
android:name=".gui.RaceTasks"
|
||||
android:exported="false" />
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
go_straight
|
||||
no_turn
|
||||
turn_around
|
||||
turn_left
|
||||
turn_right
|
||||
no_straight
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
bike
|
||||
motor
|
||||
car
|
||||
truck
|
||||
van
|
||||
bus
|
||||
Binary file not shown.
Binary file not shown.
@@ -15,5 +15,6 @@ public enum GlobalColor
|
||||
PURPLE,
|
||||
CYAN,
|
||||
BLACK,
|
||||
WHITE
|
||||
WHITE,
|
||||
INVALIDATE
|
||||
}
|
||||
|
||||
@@ -12,5 +12,6 @@ public enum GlobalShape
|
||||
CIRCLE,
|
||||
SQUARE,
|
||||
TRIANGLE,
|
||||
RECTANGLE
|
||||
RECTANGLE,
|
||||
INVALIDATE
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (c) 2023. UnknownNetworkService Group
|
||||
* This file is created by UnknownObject at 2023 - 6 - 7
|
||||
*/
|
||||
|
||||
package com.uns.maincar.data_type;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.uns.maincar.constants.GlobalColor;
|
||||
import com.uns.maincar.constants.GlobalShape;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class ShapeColorResult
|
||||
{
|
||||
private final HashMap<GlobalColor, HashMap<GlobalShape, Integer>> storage;
|
||||
|
||||
public ShapeColorResult()
|
||||
{
|
||||
storage = new HashMap<>();
|
||||
storage.put(GlobalColor.RED, new HashMap<>());
|
||||
storage.put(GlobalColor.GREEN, new HashMap<>());
|
||||
storage.put(GlobalColor.BLUE, new HashMap<>());
|
||||
storage.put(GlobalColor.YELLOW, new HashMap<>());
|
||||
storage.put(GlobalColor.PURPLE, new HashMap<>());
|
||||
storage.put(GlobalColor.CYAN, new HashMap<>());
|
||||
storage.forEach((k, v) ->
|
||||
{
|
||||
v.put(GlobalShape.STAR, 0);
|
||||
v.put(GlobalShape.CIRCLE, 0);
|
||||
v.put(GlobalShape.SQUARE, 0);
|
||||
v.put(GlobalShape.TRIANGLE, 0);
|
||||
v.put(GlobalShape.RECTANGLE, 0);
|
||||
});
|
||||
}
|
||||
|
||||
public void SetValue(GlobalColor color, GlobalShape shape, int value)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (storage.containsKey(color) && Objects.requireNonNull(storage.get(color)).containsKey(shape))
|
||||
Objects.requireNonNull(storage.get(color)).replace(shape, value);
|
||||
}
|
||||
catch (Exception ignored)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public int GetValue(GlobalColor color, GlobalShape shape)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (storage.containsKey(color) && Objects.requireNonNull(storage.get(color)).containsKey(shape))
|
||||
return Objects.requireNonNull(Objects.requireNonNull(storage.get(color)).get(shape));
|
||||
else
|
||||
return -1;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
|
||||
public int AllItemCount()
|
||||
{
|
||||
AtomicInteger sum = new AtomicInteger();
|
||||
storage.forEach((k, v) -> v.forEach((s_k, s_v) -> sum.addAndGet(s_v)));
|
||||
return sum.get();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
storage.clear();
|
||||
storage.put(GlobalColor.RED, new HashMap<>());
|
||||
storage.put(GlobalColor.GREEN, new HashMap<>());
|
||||
storage.put(GlobalColor.BLUE, new HashMap<>());
|
||||
storage.put(GlobalColor.YELLOW, new HashMap<>());
|
||||
storage.put(GlobalColor.PURPLE, new HashMap<>());
|
||||
storage.put(GlobalColor.CYAN, new HashMap<>());
|
||||
storage.forEach((k, v) ->
|
||||
{
|
||||
v.put(GlobalShape.STAR, 0);
|
||||
v.put(GlobalShape.CIRCLE, 0);
|
||||
v.put(GlobalShape.SQUARE, 0);
|
||||
v.put(GlobalShape.TRIANGLE, 0);
|
||||
v.put(GlobalShape.RECTANGLE, 0);
|
||||
});
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("--------------------ShapeColorResult START--------------------\n");
|
||||
sb.append("Total Item Count: ").append(AllItemCount()).append("\n");
|
||||
storage.forEach((k, v) -> v.forEach((s_k, s_v) -> sb.append("storage[").append(k).append("][").append(s_k).append("] = ").append(s_v).append("\n")));
|
||||
sb.append("---------------------ShapeColorResult END---------------------\n");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,10 @@ import com.uns.maincar.cpp_interface.QRDecoder;
|
||||
import com.uns.maincar.cpp_interface.ShapeColor;
|
||||
import com.uns.maincar.cpp_interface.TrafficLight;
|
||||
import com.uns.maincar.cpp_interface.TrafficSign;
|
||||
import com.uns.maincar.data_type.ShapeColorResult;
|
||||
import com.uns.maincar.open_source.shape.ShapeDetector;
|
||||
import com.uns.maincar.open_source.traffic_sign.YoloV5_tfLite_TSDetector;
|
||||
import com.uns.maincar.open_source.vehicle.YoloV5_tfLite_VIDDetector;
|
||||
import com.uns.maincar.tools.ImageReleaser;
|
||||
import com.uns.maincar.tools.OCRDataReleaser;
|
||||
import com.uns.maincar.tools.TextFilter;
|
||||
@@ -87,6 +91,10 @@ public class MainActivity extends AppCompatActivity
|
||||
private final String SerialPortPath = "/dev/ttyS4";
|
||||
//通信通道标志,true为Wifi,false为串口
|
||||
private final boolean CommunicationUsingWifi = true;
|
||||
//Yolo_tfLite检测模型对象 - 交通标志 - 来自开源项目
|
||||
private static final YoloV5_tfLite_TSDetector TS_Detector = new YoloV5_tfLite_TSDetector();
|
||||
//Yolo_tfLite检测模型对象 - 车型 - 来自于开源项目
|
||||
private static final YoloV5_tfLite_VIDDetector VID_Detector = new YoloV5_tfLite_VIDDetector();
|
||||
//调试用16进制数组
|
||||
private final String[] byte_str = {
|
||||
"0x00", "0x01", "0x02", "0x03", "0x04", "0x05", "0x06", "0x07", "0x08", "0x09", "0x0A", "0x0B", "0x0C", "0x0D", "0x0E", "0x0F",
|
||||
@@ -614,6 +622,26 @@ public class MainActivity extends AppCompatActivity
|
||||
dtc_client.CloseConnection(); //关闭通信
|
||||
throw new NullPointerException(); //通过异常来崩溃。
|
||||
});
|
||||
|
||||
context.findViewById(R.id.btn_os_shapecolor).setOnClickListener(view ->
|
||||
{
|
||||
ShapeDetector detector = new ShapeDetector();
|
||||
detector.shapePicProcess(currImage);
|
||||
ShapeColorResult result = detector.GetAllResult();
|
||||
ToastLog(result.toString(), false, false);
|
||||
});
|
||||
|
||||
context.findViewById(R.id.btn_os_trafficsign).setOnClickListener(view ->
|
||||
{
|
||||
String res = TS_Detector.processImage(currImage);
|
||||
ToastLog("Traffic Sign Result: " + res, false, false);
|
||||
});
|
||||
|
||||
context.findViewById(R.id.btn_os_vehicle).setOnClickListener(view ->
|
||||
{
|
||||
String res = TS_Detector.processImage(currImage);
|
||||
ToastLog("Vehicle Result: " + res, false, false);
|
||||
});
|
||||
}
|
||||
//----------------------------------------到此处终止----------------------------------------
|
||||
|
||||
@@ -747,6 +775,14 @@ public class MainActivity extends AppCompatActivity
|
||||
//二维码扫描自检
|
||||
ToastLog(QRDecoder.SelfTest(BitmapFactory.decodeResource(getResources(), R.drawable.qr_decode_test)), false, false);
|
||||
|
||||
//初始化开源交通标志识别库
|
||||
YoloV5_tfLite_TSDetector.minimumConfidence = 0.7f;
|
||||
ToastLog("Open Source Traffic Sign Detector: " + (TS_Detector.LoadModel("CPU", 4, this.getAssets()) ? "Success" : "Failure"), false, false);
|
||||
|
||||
//初始化开源车型识别库
|
||||
YoloV5_tfLite_VIDDetector.minimumConfidence = 0.7f;
|
||||
ToastLog("Open Source Vehicle Detector: " + (VID_Detector.LoadModel("CPU", 4, this.getAssets()) ? "Success" : "Failure"), false, false);
|
||||
|
||||
//获取主车IP地址
|
||||
wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
||||
dhcpInfo = wifiManager.getDhcpInfo();
|
||||
|
||||
@@ -0,0 +1,444 @@
|
||||
/*
|
||||
* Copyright (c) https://github.com/gh-xiao/EmbeddedCar
|
||||
* This file is pull from GitHub open source project
|
||||
* Integrated by UnknownObject at 2023 - 6 - 7
|
||||
* Value/Result Interface changed by UnknownObject at 2023 - 6 - 7
|
||||
*/
|
||||
|
||||
package com.uns.maincar.open_source.shape;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.util.Log;
|
||||
|
||||
import com.uns.maincar.constants.GlobalColor;
|
||||
import com.uns.maincar.constants.GlobalShape;
|
||||
import com.uns.maincar.data_type.ShapeColorResult;
|
||||
import com.uns.maincar.open_source.utils.BitmapProcess;
|
||||
import com.uns.maincar.open_source.utils.ColorHSV;
|
||||
import com.uns.maincar.open_source.utils.ShapeStatistics;
|
||||
|
||||
import org.opencv.android.Utils;
|
||||
import org.opencv.core.Core;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.core.MatOfPoint;
|
||||
import org.opencv.core.MatOfPoint2f;
|
||||
import org.opencv.core.Point;
|
||||
import org.opencv.core.RotatedRect;
|
||||
import org.opencv.core.Scalar;
|
||||
import org.opencv.core.Size;
|
||||
import org.opencv.imgproc.Imgproc;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ShapeDetector
|
||||
{
|
||||
//目标类的简写名称
|
||||
private static final String TAG = ShapeDetector.class.getSimpleName();
|
||||
//轮廓绘制/轮廓统计
|
||||
private static final List<MatOfPoint> contours = new ArrayList<>();
|
||||
//HashMap<颜色,HashMap<形状,数量>>
|
||||
private final HashMap<String, ShapeStatistics> ColorCounts = new HashMap<>();
|
||||
//检测出的所有图形数量
|
||||
private int totals = 0;
|
||||
//使用枚举类型的结果储存
|
||||
ShapeColorResult result = new ShapeColorResult();
|
||||
|
||||
public int GetSpecItemCount(GlobalColor color, GlobalShape shape)
|
||||
{
|
||||
return result.GetValue(color, shape);
|
||||
}
|
||||
|
||||
public int GetAllItemCount()
|
||||
{
|
||||
return result.AllItemCount();
|
||||
}
|
||||
|
||||
public ShapeColorResult GetAllResult()
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取该图片中所有的图形数量
|
||||
*/
|
||||
public int getTotals()
|
||||
{
|
||||
for (Map.Entry<String, ShapeStatistics> map : ColorCounts.entrySet())
|
||||
{
|
||||
totals += map.getValue().getCounts("总计");
|
||||
}
|
||||
return totals;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置统计
|
||||
*
|
||||
* @param totals 0
|
||||
*/
|
||||
public void setTotals(int totals)
|
||||
{
|
||||
this.totals = totals;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定颜色的统计对象
|
||||
*
|
||||
* @return 该颜色的统计对象<形状, 数量>
|
||||
*/
|
||||
public HashMap<String, ShapeStatistics> getColorCounts()
|
||||
{
|
||||
return ColorCounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定图形的数量
|
||||
*/
|
||||
public int getShapeCounts(String shapeName)
|
||||
{
|
||||
int counts = 0;
|
||||
for (Map.Entry<String, ShapeStatistics> map : ColorCounts.entrySet())
|
||||
{
|
||||
counts += map.getValue().getCounts(shapeName);
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 形状识别 - Bitmap图片处理
|
||||
*
|
||||
* @param inputBitmap 需要处理的图片
|
||||
*/
|
||||
public void shapePicProcess(Bitmap inputBitmap)
|
||||
{
|
||||
if (inputBitmap == null) return;
|
||||
/* openCV创建用来存储图像信息的内存对象 */
|
||||
Mat srcMat = new Mat();
|
||||
/* 转化为Mat对象 */
|
||||
Utils.bitmapToMat(inputBitmap, srcMat);
|
||||
shapePicProcess(srcMat);
|
||||
}
|
||||
|
||||
/**
|
||||
* 形状识别 - Mat图片处理
|
||||
*
|
||||
* @param srcMat 需要识别的图片
|
||||
*/
|
||||
public void shapePicProcess(Mat srcMat)
|
||||
{
|
||||
if (srcMat == null)
|
||||
return;
|
||||
ColorCounts.clear();
|
||||
/* 保存用 */
|
||||
BitmapProcess.saveBitmap("TFTAutoCutter", srcMat);
|
||||
/* 颜色形状分析 */
|
||||
Identify(srcMat, ColorHSV.yellowHSV1, "黄色");
|
||||
Identify(srcMat, ColorHSV.greenHSV1, "绿色");
|
||||
Identify(srcMat, ColorHSV.cyanHSV, "青色");
|
||||
Identify(srcMat, ColorHSV.blueHSV3, "蓝色");
|
||||
Identify(srcMat, ColorHSV.purpleHSV2, "紫色");
|
||||
/* 红色颜色取反,方便处理 */
|
||||
Identify(srcMat, "红色");
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>形状识别 - 反色处理</p>
|
||||
* <p>因红色阈值问题,建议将图片进行反色处理</p>
|
||||
*
|
||||
* @param inputMat 已经处理的Mat对象
|
||||
*/
|
||||
private void Identify(Mat inputMat, @SuppressWarnings("SameParameterValue") String colorName)
|
||||
{
|
||||
Mat dstMat = new Mat();
|
||||
//RGB转换为BGR - 红蓝色互换
|
||||
Imgproc.cvtColor(inputMat, dstMat, Imgproc.COLOR_BGR2RGB);
|
||||
Identify(dstMat, ColorHSV.red2blueHSV, colorName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 形状识别
|
||||
*
|
||||
* @param Mtmp 已经处理的Mat对象
|
||||
* @param r 色彩数据
|
||||
* @param colorName 色彩名
|
||||
*/
|
||||
private void Identify(Mat Mtmp, int[] r, String colorName)
|
||||
{
|
||||
/* openCV创建用来存储图像信息的内存对象 */
|
||||
Mat hsvMat = new Mat();
|
||||
Mat outMat = new Mat();
|
||||
Mat mat = Mtmp.clone();
|
||||
/* 转换为HSV */
|
||||
Imgproc.cvtColor(mat, hsvMat, Imgproc.COLOR_RGB2HSV);
|
||||
/* 颜色分割 */
|
||||
Core.inRange(hsvMat, new Scalar(r[2], r[4], r[6]), new Scalar(r[1], r[3], r[5]), hsvMat);
|
||||
/* 确定运算核,类似于卷积核 */
|
||||
Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3));
|
||||
/* 开运算(除去白噪点) */
|
||||
Imgproc.morphologyEx(hsvMat, hsvMat, Imgproc.MORPH_OPEN, kernel);
|
||||
/* 闭运算(除去黑噪点) */
|
||||
// Imgproc.morphologyEx(hsvMat, hsvMat, Imgproc.MORPH_CLOSE, kernel);
|
||||
/* 轮廓提取,用于提取图像的轮廓 */
|
||||
contours.clear();
|
||||
Imgproc.findContours(hsvMat, contours, outMat, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
|
||||
/* 绘制轮廓,用于绘制找到的图像轮廓 */
|
||||
/*
|
||||
* 函数参数详解:
|
||||
* 第一个参数image表示目标图像
|
||||
* 第二个参数contours表示输⼊的轮廓组,每⼀组轮廓由点vector构成
|
||||
* 第三个参数contourIdx指明画第⼏个轮廓,如果该参数为负值,则画全部轮廓
|
||||
* 第四个参数color为轮廓的颜色
|
||||
* 第五个参数thickness为轮廓的线宽,如果为负值或CV_FILLED表⽰填充轮廓内部
|
||||
*/
|
||||
Imgproc.drawContours(mat, contours, -1, new Scalar(0, 255, 0), 2);
|
||||
/* 形状统计 */
|
||||
/* 核心统计代码,参数已调整 */
|
||||
//轮廓
|
||||
MatOfPoint2f contour2f;
|
||||
//近似曲线(多边形拟合)
|
||||
MatOfPoint2f approxCurve;
|
||||
/* 逼近的精度(阈值),设定的原始曲线与近似曲线之间的最大距离 */
|
||||
double epsilon;
|
||||
int tri, rect, circle, star, rhombus;
|
||||
tri = rect = circle = star = rhombus = 0;
|
||||
Log.e(TAG, "----------" + colorName + "总计轮廓: " + contours.size() + "----------");
|
||||
/* 遍历轮廓 */
|
||||
for (int i = 0; i < contours.size(); i++)
|
||||
{
|
||||
/* 判断面积是否大于阈值(有效图形) */
|
||||
if (Imgproc.contourArea(contours.get(i)) > 200)
|
||||
{
|
||||
Log.i(TAG, "查找到有效轮廓,面积为: " + Imgproc.contourArea(contours.get(i)));
|
||||
/* 某一个点的集合(当前对象的轮廓) */
|
||||
contour2f = new MatOfPoint2f(contours.get(i).toArray());
|
||||
/*
|
||||
* 计算轮廓的周长
|
||||
* 0.035这个系数是一个精度因子,用来控制近似多边形的形状。
|
||||
* 它越小,近似多边形就越接近原始轮廓。它越大,近似多边形就越简单,有更少的顶点。你可以根据你的需要调整这个系数。
|
||||
* by New Bing
|
||||
*/
|
||||
epsilon = 0.045 * Imgproc.arcLength(contour2f, true);
|
||||
//多边形拟合后的轮廓
|
||||
approxCurve = new MatOfPoint2f();
|
||||
/* 多边形拟合 */
|
||||
Imgproc.approxPolyDP(contour2f, approxCurve, epsilon, true);
|
||||
/* boundingRect获取不带旋转角度的最小外接矩形 */
|
||||
// Rect minRect = Imgproc.boundingRect(approxCurve);
|
||||
/* 绘制不带旋转角度的外接矩形,并计算外接矩形的轮廓中心 */
|
||||
// Imgproc.rectangle(mat, minRect, new Scalar(255, 255, 0), 1);
|
||||
/* minAreaRect获得带旋转角度的最小外接矩形 */
|
||||
RotatedRect minRotatedRect = Imgproc.minAreaRect(approxCurve);
|
||||
/* 获取顶点数据 */
|
||||
Point[] box = new Point[4];
|
||||
minRotatedRect.points(box);
|
||||
/* 将顶点转换为整数类型 */
|
||||
MatOfPoint boxInt = new MatOfPoint();
|
||||
boxInt.fromArray(box);
|
||||
/* 绘制带旋转角度的外接矩形,并计算外接矩形的轮廓中心 */
|
||||
Imgproc.polylines(mat, Collections.singletonList(boxInt), true, new Scalar(255, 255, 255));
|
||||
Log.i(TAG, "包含角点数: " + approxCurve.rows());
|
||||
if (approxCurve.rows() == 3) tri++;
|
||||
/* 判断矩形和菱形 */
|
||||
/* 面积判断法 - 旧 */
|
||||
// else if (approxCurve.rows() == 4) {
|
||||
// double area, minArea;
|
||||
// /* 该图形(四边形)拟合的面积 */
|
||||
// area = Imgproc.contourArea(approxCurve);
|
||||
// /* 包含旋转角度的最小外接矩形的面积 */
|
||||
// RotatedRect minAreaRect = Imgproc.minAreaRect(approxCurve);
|
||||
// minArea = minAreaRect.size.area();
|
||||
// /* 图形面积/外接矩形面积 */
|
||||
// double rec = area / minArea;
|
||||
// Log.i(TAG, "这是area / minArea得到的阈值: " + rec);
|
||||
// if (rec >= 0.80 && rec < 1.15) rect++;
|
||||
// else rhombus++;
|
||||
// }
|
||||
/* 判断菱形 - 边长 */
|
||||
else if (isRhombus(approxCurve)) rhombus++;
|
||||
/* 判断矩形 - 对角线 */
|
||||
else if (isRectangle(approxCurve)) rect++;
|
||||
/* 判断五角星和圆形 - 面积 */
|
||||
// else if (approxCurve.rows() > 4) {
|
||||
// /* 最小外接矩形的面积 */
|
||||
// int minAreaRect = minRect.height * minRect.width;
|
||||
// /* 该图形面积 */
|
||||
// double area = Imgproc.contourArea(contours.get(i));
|
||||
// if ((area / minAreaRect) > 0.5) circle++;
|
||||
// else star++;
|
||||
// }
|
||||
/* 判断五角星和圆形 - 圆形度 */
|
||||
else if (approxCurve.rows() > 4)
|
||||
{
|
||||
/* 该图形面积 */
|
||||
double area = Imgproc.contourArea(contours.get(i));
|
||||
/* 该图形周长 */
|
||||
double len = Imgproc.arcLength(approxCurve, true);
|
||||
/* 圆形度 */
|
||||
double roundness = (4 * Math.PI * area) / (len * len);
|
||||
Log.i(TAG, "该图形的圆形度: " + roundness);
|
||||
if (roundness > 0.8) circle++;
|
||||
else star++;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* 引用ShapeCount对象存放识别数据 */
|
||||
//SaveResult(colorName, circle, tri, rect, star, rhombus);
|
||||
|
||||
//使用枚举类型的数据存储,这样更便于查询
|
||||
BetterSaveResult(colorName, circle, tri, rect, star, rhombus);
|
||||
/* 输出结果 */
|
||||
String msg = "圆形: " + circle + " 三角形: " + tri + " 矩形: " + rect + " 菱形: " + rhombus + " 五角星: " + star;
|
||||
Log.e(TAG, msg);
|
||||
/* 保存图片 */
|
||||
BitmapProcess.saveBitmap(colorName, mat);
|
||||
Log.e(TAG, "----------" + colorName + "识别完成----------");
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断一个多边形是否为菱形
|
||||
*
|
||||
* @param approxCurve MatOfPoint2f
|
||||
* @return boolean
|
||||
*/
|
||||
private static boolean isRhombus(MatOfPoint2f approxCurve)
|
||||
{
|
||||
/* 如果顶点数为4,则可能为菱形 */
|
||||
if (approxCurve.toArray().length == 4)
|
||||
{
|
||||
/* 获取顶点坐标 */
|
||||
Point[] points = approxCurve.toArray();
|
||||
/* 获取边长长度 */
|
||||
double l1 = getDistance(points[0], points[1]);
|
||||
double l2 = getDistance(points[1], points[2]);
|
||||
double l3 = getDistance(points[2], points[3]);
|
||||
double l4 = getDistance(points[3], points[0]);
|
||||
Log.i(TAG, "该轮廓四边边长(顺时针):\n" + "■■■" + l1 + "■■■" + l2 + "■■■\n■■■" + l4 + "■■■" + l3 + "■■■");
|
||||
/* 轮廓的邻边边长在误差范围内相等则为菱形 */
|
||||
return Math.abs(l1 - l2) < 5 && Math.abs(l2 - l3) < 5 && Math.abs(l3 - l4) < 5 && Math.abs(l4 - l1) < 5;
|
||||
}
|
||||
// 不是菱形
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断一个多边形是否为矩形
|
||||
*
|
||||
* @param approxCurve MatOfPoint2f
|
||||
* @return boolean
|
||||
*/
|
||||
private static boolean isRectangle(MatOfPoint2f approxCurve)
|
||||
{
|
||||
/* 如果顶点数为4,则可能为矩形 */
|
||||
if (approxCurve.toArray().length == 4)
|
||||
{
|
||||
/* 计算四个顶点之间的距离 */
|
||||
// double d1 = getDistance(approxCurve.toArray()[0], approxCurve.toArray()[1]);
|
||||
// double d2 = getDistance(approxCurve.toArray()[1], approxCurve.toArray()[2]);
|
||||
// double d3 = getDistance(approxCurve.toArray()[2], approxCurve.toArray()[3]);
|
||||
// double d4 = getDistance(approxCurve.toArray()[3], approxCurve.toArray()[0]);
|
||||
/* 计算对角线之间的距离 */
|
||||
double d5 = getDistance(approxCurve.toArray()[0], approxCurve.toArray()[2]);
|
||||
double d6 = getDistance(approxCurve.toArray()[1], approxCurve.toArray()[3]);
|
||||
/* 判断对角线是否相等,并且相邻边是否垂直(即乘积为零) */
|
||||
// double vector = Math.abs(d1 * d2 + d2 * d3 + d3 * d4 + d4 * d1);
|
||||
// Log.e(TAG, "对角线长度比对: " + Math.abs(d5 - d6) + "邻边角度误差: " + vector);
|
||||
// return Math.abs(d5 - d6) < 1e-6 && vector < 1e-6; // 是矩形
|
||||
/* 判断对角线是否相等 */
|
||||
Log.e(TAG, "对角线长度比对: " + Math.abs(d5 - d6));
|
||||
return Math.abs(d5 - d6) < 3; // 是矩形
|
||||
}
|
||||
// 不是矩形
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两点之间的距离
|
||||
*
|
||||
* @param p1 -
|
||||
* @param p2 -
|
||||
* @return 两点距离
|
||||
*/
|
||||
private static double getDistance(Point p1, Point p2)
|
||||
{
|
||||
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* 形状数据保存
|
||||
*
|
||||
* @param colorName 形状颜色
|
||||
* @param circle -
|
||||
* @param tri -
|
||||
* @param rect -
|
||||
* @param star -
|
||||
* @param rhombus -
|
||||
*/
|
||||
/*private void SaveResult(String colorName, int circle, int tri, int rect, int star, int rhombus)
|
||||
{
|
||||
*//* 保存该颜色包含的图形统计 *//*
|
||||
HashMap<String, Integer> hashMap = new HashMap<>();
|
||||
hashMap.put("三角形", tri);
|
||||
hashMap.put("矩形", rect);
|
||||
hashMap.put("菱形", rhombus);
|
||||
hashMap.put("五角星", star);
|
||||
hashMap.put("圆形", circle);
|
||||
hashMap.put("总计", tri + rect + rhombus + star + circle);
|
||||
*//* 形状计数对象 *//*
|
||||
ShapeStatistics statistics = new ShapeStatistics();
|
||||
*//* 保存在该对象上 *//*
|
||||
statistics.setShapeStatistics(hashMap);
|
||||
ColorCounts.put(colorName, statistics);
|
||||
}*/
|
||||
|
||||
/**
|
||||
* 将字符串颜色名转换为枚举类型
|
||||
*
|
||||
* @param color_name 颜色名称
|
||||
*/
|
||||
private GlobalColor ConvertColorName(String color_name)
|
||||
{
|
||||
switch (color_name)
|
||||
{
|
||||
case "红色":
|
||||
return GlobalColor.RED;
|
||||
case "黄色":
|
||||
return GlobalColor.YELLOW;
|
||||
case "绿色":
|
||||
return GlobalColor.GREEN;
|
||||
case "青色":
|
||||
return GlobalColor.CYAN;
|
||||
case "蓝色":
|
||||
return GlobalColor.BLUE;
|
||||
case "紫色":
|
||||
return GlobalColor.PURPLE;
|
||||
}
|
||||
return GlobalColor.INVALIDATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用枚举类型的数据保存
|
||||
* “我特地重写了这个类的数据保存逻辑,因为直接使用描述字符串作为Key来存储信息实在不是一个好主意。” ——UnknownObject@2023-06-07
|
||||
*
|
||||
* @param colorName 颜色名称
|
||||
* @param circle 圆
|
||||
* @param tri 三角
|
||||
* @param rect 矩形
|
||||
* @param star 五角星
|
||||
* @param rhombus 菱形
|
||||
*/
|
||||
private void BetterSaveResult(String colorName, int circle, int tri, int rect, int star, int rhombus)
|
||||
{
|
||||
GlobalColor color = ConvertColorName(colorName);
|
||||
if (color != GlobalColor.INVALIDATE)
|
||||
{
|
||||
result.SetValue(color, GlobalShape.CIRCLE, circle);
|
||||
result.SetValue(color, GlobalShape.TRIANGLE, tri);
|
||||
result.SetValue(color, GlobalShape.RECTANGLE, rect);
|
||||
result.SetValue(color, GlobalShape.STAR, star);
|
||||
result.SetValue(color, GlobalShape.SQUARE, rhombus);
|
||||
}
|
||||
}
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Copyright (c) https://github.com/gh-xiao/EmbeddedCar
|
||||
* This file is pull from GitHub open source project
|
||||
* Integrated by UnknownObject at 2023 - 6 - 7
|
||||
*/
|
||||
|
||||
package com.uns.maincar.open_source.traffic_sign;
|
||||
|
||||
import android.content.res.AssetManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.uns.maincar.open_source.utils.BitmapProcess;
|
||||
|
||||
import org.tensorflow.lite.examples.detection.env.Logger;
|
||||
import org.tensorflow.lite.examples.detection.tflite.Classifier;
|
||||
import org.tensorflow.lite.examples.detection.tflite.DetectorFactory;
|
||||
import org.tensorflow.lite.examples.detection.tflite.YoloV5Classifier;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 使用基于YoloV5-tfLite模型的交通标志物识别
|
||||
*/
|
||||
public class YoloV5_tfLite_TSDetector
|
||||
{
|
||||
|
||||
// Which detection model to use: by default uses Tensorflow Object Detection API frozen
|
||||
// checkpoints.
|
||||
// enum DetectorMode {TF_OD_API}
|
||||
|
||||
//日志对象
|
||||
private static final Logger LOGGER = new Logger();
|
||||
//枚举常量 - 检测模式
|
||||
// private static final DetectorMode MODE = DetectorMode.TF_OD_API;
|
||||
public static final float MINIMUM_CONFIDENCE_TF_OD_API = 0.35f;
|
||||
//最小置信度
|
||||
public static float minimumConfidence;
|
||||
//核心检测对象
|
||||
private YoloV5Classifier detector;
|
||||
//模型列表
|
||||
private final String[] models = new String[]{"TSyolov5s-fp16.tflite", "TSyolov5s-fp16-3.tflite", "TSyolov5s-fp16-byGray.tflite"};
|
||||
//检测图片
|
||||
private Bitmap SaveBitmap;
|
||||
private long timestamp = 0;
|
||||
|
||||
public Bitmap getSaveBitmap()
|
||||
{
|
||||
return SaveBitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载模型配置
|
||||
*
|
||||
* @param device 使用何种硬件加载
|
||||
* @param numThreads 使用多少线程加载
|
||||
* @param assetManager AssetManager管理对象
|
||||
*/
|
||||
public boolean LoadModel(String device, int numThreads, AssetManager assetManager)
|
||||
{
|
||||
|
||||
//模型文件
|
||||
String modelString = models[2];
|
||||
//检测类别(标签)
|
||||
String labelFilename = "TSclass.txt";
|
||||
/* 线程数(不推荐超过9线程数) */
|
||||
if (numThreads > 9) numThreads = 4;
|
||||
LOGGER.i("Changing model to ***" + modelString + "*** device ***" + device + "***");
|
||||
|
||||
/* Try to load model. */
|
||||
/* 尝试加载模型 */
|
||||
try
|
||||
{
|
||||
detector = DetectorFactory.getDetector(assetManager, modelString, labelFilename);
|
||||
// Customize the interpreter to the type of device we want to use.
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
LOGGER.e(e, "Exception in updateActiveModel()");
|
||||
// Toast toast = Toast.makeText(FirstActivity.getContext(), "Classifier could not be initialized", Toast.LENGTH_SHORT);
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (device)
|
||||
{
|
||||
case "GPU":
|
||||
detector.useGpu();
|
||||
break;
|
||||
case "NNAPI":
|
||||
detector.useNNAPI();
|
||||
break;
|
||||
default:
|
||||
detector.useCPU();
|
||||
break;
|
||||
}
|
||||
/* 设置线程数 */
|
||||
detector.setNumThreads(numThreads);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测图片
|
||||
*
|
||||
* @param inputBitmap -
|
||||
* @return Gson字符串
|
||||
*/
|
||||
public String processImage(Bitmap inputBitmap)
|
||||
{
|
||||
/* 结果列表对象 */
|
||||
List<Classifier.Recognition> recognitions = new LinkedList<>();
|
||||
/* 将结果转换成Gson */
|
||||
Gson gson = new Gson();
|
||||
|
||||
if (inputBitmap == null) return gson.toJson(recognitions);
|
||||
//416*416
|
||||
int cropSize = detector.getInputSize();
|
||||
System.out.println(cropSize);
|
||||
|
||||
int width = inputBitmap.getWidth();
|
||||
int height = inputBitmap.getHeight();
|
||||
float scaleWidth = ((float) cropSize) / width;
|
||||
float scaleHeight = ((float) cropSize) / height;
|
||||
//矩阵
|
||||
Matrix matrix = new Matrix();
|
||||
matrix.postScale(scaleWidth, scaleHeight);
|
||||
|
||||
/* 将输入图片通过矩阵变换得到416*416大小的新图片 */
|
||||
Bitmap croppedBitmap = Bitmap.createBitmap(inputBitmap, 0, 0, width, height, matrix, true);
|
||||
/* 灰度化图像 */
|
||||
croppedBitmap = BitmapProcess.GrayscaleImage(croppedBitmap);
|
||||
/* 设置输出结果图像(在该图像上绘制识别结果) */
|
||||
Bitmap draw = croppedBitmap.copy(Bitmap.Config.ARGB_8888, true);
|
||||
|
||||
++timestamp;
|
||||
final long currTimestamp = timestamp;
|
||||
|
||||
LOGGER.i("Preparing image " + currTimestamp + " for detection in bg thread.");
|
||||
/* 利用分类器classifier对图片进行预测分析,得到图片为每个分类的概率. 比较耗时 */
|
||||
LOGGER.i("Running detection on image " + currTimestamp);
|
||||
|
||||
final long startTime = SystemClock.uptimeMillis();
|
||||
/* 核心检测 */
|
||||
final List<Classifier.Recognition> results = detector.recognizeImage(croppedBitmap);
|
||||
/* 计算检测时间 */
|
||||
long lastProcessingTimeMs = SystemClock.uptimeMillis() - startTime;
|
||||
/* 检测出多少对象 */
|
||||
Log.e("CHECK", "run: " + results.size());
|
||||
/* 检测时间 */
|
||||
Log.i("Time Spent: ", lastProcessingTimeMs + "ms");
|
||||
|
||||
/* 筛选通过最低置信度阈值的识别结果 */
|
||||
final List<Classifier.Recognition> mappedRecognitions = new LinkedList<>();
|
||||
for (final Classifier.Recognition result : results)
|
||||
{
|
||||
final RectF location = result.getLocation();
|
||||
if (location != null && result.getConfidence() >= minimumConfidence)
|
||||
{
|
||||
result.setLocation(location);
|
||||
/* 将通过最低置信度的结果添加到新List */
|
||||
mappedRecognitions.add(result);
|
||||
//识别结果
|
||||
Log.e("result: ", result.getTitle() + result.getConfidence());
|
||||
drawBitmap(result, draw);
|
||||
}
|
||||
}
|
||||
|
||||
return gson.toJson(mappedRecognitions.size() > 0 ? mappedRecognitions : recognitions);
|
||||
}
|
||||
|
||||
private void drawBitmap(Classifier.Recognition result, Bitmap resultBitmap)
|
||||
{
|
||||
final Canvas canvas = new Canvas(resultBitmap);
|
||||
final Paint paint = new Paint();
|
||||
paint.setColor(Color.RED);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeWidth(2.0f);
|
||||
canvas.drawRect(result.getLocation(), paint);
|
||||
SaveBitmap = resultBitmap.copy(Bitmap.Config.ARGB_8888, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
/*
|
||||
* Copyright (c) https://github.com/gh-xiao/EmbeddedCar
|
||||
* This file is pull from GitHub open source project
|
||||
* Integrated by UnknownObject at 2023 - 6 - 7
|
||||
*/
|
||||
|
||||
package com.uns.maincar.open_source.utils;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
|
||||
import org.opencv.android.Utils;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.imgcodecs.Imgcodecs;
|
||||
import org.opencv.imgproc.Imgproc;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
public class BitmapProcess
|
||||
{
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private static BitmapProcess mInstance;
|
||||
private Context mContext;
|
||||
// 指定我们想要存储文件的地址
|
||||
public static final String TargetPath = Environment.getExternalStorageDirectory() + "/" + Environment.DIRECTORY_DCIM + "/Tess/";
|
||||
// 获取时间
|
||||
private static final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss", Locale.CHINA);
|
||||
|
||||
private BitmapProcess()
|
||||
{
|
||||
}
|
||||
|
||||
public static synchronized BitmapProcess getInstance()
|
||||
{
|
||||
if (mInstance == null)
|
||||
{
|
||||
mInstance = new BitmapProcess();
|
||||
}
|
||||
return mInstance;
|
||||
}
|
||||
|
||||
public void init(Context context)
|
||||
{
|
||||
this.mContext = context.getApplicationContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局使用的图片保存方法
|
||||
*
|
||||
* @param name 图片名
|
||||
* @param mat 需要保存的mat
|
||||
*/
|
||||
public static void saveBitmap(String name, Mat mat)
|
||||
{
|
||||
if (mat == null) return;
|
||||
Bitmap bm = Bitmap.createBitmap(mat.width(), mat.height(), Bitmap.Config.ARGB_8888);
|
||||
Utils.matToBitmap(mat, bm);
|
||||
saveBitmap(name, bm);
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局使用的图片保存方法
|
||||
*
|
||||
* @param name 图片名
|
||||
* @param bm 需要保存的Bitmap
|
||||
* @return 是否保存成功
|
||||
*/
|
||||
public static String saveBitmap(String name, Bitmap bm)
|
||||
{
|
||||
if (bm == null) return "错误,没有图片!";
|
||||
return Build.VERSION.SDK_INT < 29 ? saveImageOld(name, bm) : mInstance.saveImageNew(name, bm);
|
||||
}
|
||||
|
||||
/**
|
||||
* 旧版本Android保存图片方法
|
||||
*
|
||||
* @param name 图片名
|
||||
* @param bm 等待保存的Bitmap
|
||||
* @return 是否保存成功
|
||||
*/
|
||||
private static String saveImageOld(String name, Bitmap bm)
|
||||
{
|
||||
Log.d("Save Bitmap", "Ready to save picture");
|
||||
StringBuilder append = new StringBuilder().append("Save Path = ");
|
||||
String str = TargetPath;
|
||||
Log.d("Save Bitmap", append.append(str).toString());
|
||||
if (fileIsExist())
|
||||
{
|
||||
try
|
||||
{
|
||||
FileOutputStream saveImgOut = new FileOutputStream(new File(str, name + "-" + format.format(new Date()) + ".jpg"));
|
||||
bm.compress(Bitmap.CompressFormat.JPEG, 80, saveImgOut);
|
||||
saveImgOut.flush();
|
||||
Log.d("Save Bitmap", "The picture is save to your phone!");
|
||||
return "保存完毕!";
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
ex.printStackTrace();
|
||||
return "IOException!";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.d("Save Bitmap", "TargetPath isn't exist");
|
||||
return "TargetPath isn't exist!";
|
||||
}
|
||||
}
|
||||
|
||||
static boolean fileIsExist()
|
||||
{
|
||||
File file = new File(TargetPath);
|
||||
return file.exists() || file.mkdirs();
|
||||
}
|
||||
|
||||
/**
|
||||
* 高版本Android保存图片方法
|
||||
*
|
||||
* @param name 图片名
|
||||
* @param bm 等待保存的Bitmap
|
||||
* @return 是否保存成功
|
||||
*/
|
||||
private String saveImageNew(String name, Bitmap bm)
|
||||
{
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put("_display_name", name + format.format(new Date()));
|
||||
contentValues.put("description", name);
|
||||
contentValues.put("mime_type", "image/jpeg");
|
||||
contentValues.put("relative_path", "DCIM/Tess");
|
||||
try (OutputStream outputStream = mContext.getContentResolver()
|
||||
.openOutputStream(mContext.getContentResolver()
|
||||
.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)))
|
||||
{
|
||||
bm.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
|
||||
Log.d("Save Bitmap", "Save success!");
|
||||
return "保存完毕!";
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
Log.d("Save Bitmap", "Save fail!");
|
||||
return "Exception!";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过URL获取图片
|
||||
*
|
||||
* @param uri -
|
||||
* @return Bitmap
|
||||
*/
|
||||
public Bitmap showImage(Uri uri)
|
||||
{
|
||||
try (ParcelFileDescriptor parcelFileDescriptor = mContext.getContentResolver().openFileDescriptor(uri, "r"))
|
||||
{
|
||||
return BitmapFactory.decodeFileDescriptor(parcelFileDescriptor.getFileDescriptor());
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过图片名获取Tess文件夹对应的图片
|
||||
*
|
||||
* @param imageName 图片名
|
||||
* @return Bitmap
|
||||
*/
|
||||
public static Bitmap getImages(String imageName)
|
||||
{
|
||||
String path = TargetPath + imageName;
|
||||
return new File(path).exists() ? BitmapFactory.decodeFile(path) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过绝对路径获取Bitmap
|
||||
*
|
||||
* @param realPath 绝对路径
|
||||
* @return Bitmap
|
||||
*/
|
||||
public static Bitmap getRealPathImages(String realPath)
|
||||
{
|
||||
return new File(realPath).exists() ? BitmapFactory.decodeFile(realPath) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 灰度化图像
|
||||
*
|
||||
* @param inputBitmap 需要灰度化图像的Bitmap
|
||||
* @return 灰度化后的Bitmap
|
||||
*/
|
||||
public static Bitmap GrayscaleImage(Bitmap inputBitmap)
|
||||
{
|
||||
if (inputBitmap == null) return null;
|
||||
Mat mat = new Mat();
|
||||
Utils.bitmapToMat(inputBitmap, mat);
|
||||
return GrayscaleImage(mat);
|
||||
}
|
||||
|
||||
/**
|
||||
* 灰度化图像
|
||||
*
|
||||
* @param colorImage 需要灰度化图像的Bitmap
|
||||
* @return 灰度化后的Bitmap
|
||||
*/
|
||||
public static Bitmap GrayscaleImage(Mat colorImage)
|
||||
{
|
||||
if (colorImage == null) return null;
|
||||
// 创建一个空的灰度图像
|
||||
Mat grayscaleImage = new Mat();
|
||||
// 调用cvtColor函数,将彩色图像转换为灰度图像
|
||||
Imgproc.cvtColor(colorImage, grayscaleImage, Imgproc.COLOR_RGB2GRAY);
|
||||
Bitmap result = Bitmap.createBitmap(colorImage.width(), colorImage.height(), Bitmap.Config.ARGB_8888);
|
||||
Utils.matToBitmap(grayscaleImage, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 灰度化图像并保存
|
||||
*/
|
||||
private void GrayscaleImage()
|
||||
{
|
||||
// 定义源文件夹和目标文件夹的路径
|
||||
String sourcePath = Environment.getExternalStorageDirectory() + "/" + Environment.DIRECTORY_DCIM + "/srcImg/";
|
||||
String targetPath = Environment.getExternalStorageDirectory() + "/" + Environment.DIRECTORY_DCIM + "/dstImg/";
|
||||
// 创建File对象,表示源文件夹和目标文件夹
|
||||
File sourceFolder = new File(sourcePath);
|
||||
File targetFolder = new File(targetPath);
|
||||
// 检查源文件夹是否存在,如果不存在,打印错误信息并退出
|
||||
if (!sourceFolder.exists())
|
||||
{
|
||||
System.out.println("Source folder does not exist.");
|
||||
return;
|
||||
}
|
||||
// 检查目标文件夹是否存在,如果不存在,就创建一个
|
||||
if (!targetFolder.exists()) if (!targetFolder.mkdir()) return;
|
||||
// 获取源文件夹中的所有文件,存放在一个File数组中
|
||||
File[] files = sourceFolder.listFiles();
|
||||
if (files == null) return;
|
||||
// 遍历File数组,对每个文件进行灰度化处理
|
||||
for (File file : files)
|
||||
{
|
||||
// 获取文件
|
||||
Bitmap bitmap = BitmapProcess.getRealPathImages(sourcePath + file.getName());
|
||||
if (bitmap == null) continue;
|
||||
// 加载原始图像
|
||||
Mat colorImage = new Mat();
|
||||
Utils.bitmapToMat(bitmap, colorImage);
|
||||
// 创建一个空的灰度图像
|
||||
Mat grayscaleImage = new Mat();
|
||||
// 调用cvtColor函数,将彩色图像转换为灰度图像
|
||||
Imgproc.cvtColor(colorImage, grayscaleImage, Imgproc.COLOR_RGB2GRAY);
|
||||
Utils.matToBitmap(grayscaleImage, bitmap);
|
||||
// 获取文件的名称,不包括扩展名
|
||||
StringBuilder fileName = new StringBuilder();
|
||||
for (int j = 0; j < file.getName().split("\\.").length - 1; j++)
|
||||
{
|
||||
fileName.append(file.getName().split("\\.")[j]).append(".");
|
||||
}
|
||||
Imgcodecs.imwrite(file.getName(), grayscaleImage);
|
||||
if (file.getName().split("\\.")[file.getName().split("\\.").length - 1].equals("jpg"))
|
||||
{
|
||||
try
|
||||
{
|
||||
FileOutputStream saveImgOut = new FileOutputStream(new File(targetPath, fileName + "jpg"));
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, saveImgOut);
|
||||
saveImgOut.flush();
|
||||
Log.d("Save Bitmap", "The picture is save to your phone!");
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
FileOutputStream saveImgOut = new FileOutputStream(new File(targetPath, fileName + "png"));
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, saveImgOut);
|
||||
saveImgOut.flush();
|
||||
Log.d("Save Bitmap", "The picture is save to your phone!");
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright (c) https://github.com/gh-xiao/EmbeddedCar
|
||||
* This file is pull from GitHub open source project
|
||||
* Integrated by UnknownObject at 2023 - 6 - 7
|
||||
*/
|
||||
|
||||
package com.uns.maincar.open_source.utils;
|
||||
|
||||
/**
|
||||
* <p>Hsv色彩空间范围</p>
|
||||
* <p>以下排序使用HSV的H(色彩度数)排序</p>
|
||||
* <p>数组下标为2,4,6时是最小值,下标为1,3,5时是最大值</p>
|
||||
* <p>由于HSV模型的特性,红色被划分为了两个部分,因而使用了两个数组</p>
|
||||
*/
|
||||
@SuppressWarnings("ALL")
|
||||
public final class ColorHSV
|
||||
{
|
||||
/* 图形识别色彩数据 */
|
||||
//所有H(色彩度)
|
||||
public static final int[] allHSV = new int[]{0, 180, 0, 255, 150, 255, 110};
|
||||
//红色:0-20
|
||||
public static final int[] redDownHSV = new int[]{0, 25, 0, 255, 110, 255, 110};
|
||||
public static final int[] redDownHSV1 = new int[]{0, 25, 0, 255, 150, 255, 110};
|
||||
//红色:160-180
|
||||
public static final int[] redUpHSV = new int[]{0, 180, 160, 255, 150, 255, 110};
|
||||
public static final int[] redUpHSV1 = new int[]{0, 180, 150, 255, 110, 255, 110};
|
||||
//黄色
|
||||
public static final int[] yellowHSV = new int[]{0, 43, 25, 255, 150, 255, 110};
|
||||
public static final int[] yellowHSV1 = new int[]{0, 43, 25, 255, 110, 255, 225};
|
||||
//绿色
|
||||
public static final int[] greenHSV = new int[]{0, 70, 55, 255, 150, 255, 110};
|
||||
public static final int[] greenHSV1 = new int[]{0, 70, 40, 255, 150, 255, 110};
|
||||
//青色
|
||||
public static final int[] cyanHSV = new int[]{0, 95, 85, 255, 150, 255, 110};
|
||||
public static final int[] cyanHSV1 = new int[]{0, 95, 85, 255, 190, 255, 110};
|
||||
//蓝色
|
||||
public static final int[] blueHSV = new int[]{0, 120, 110, 255, 150, 255, 110};
|
||||
public static final int[] blueHSV1 = new int[]{0, 130, 95, 255, 225, 255, 110};
|
||||
public static final int[] blueHSV2 = new int[]{0, 120, 85, 255, 210, 255, 195};
|
||||
public static final int[] blueHSV3 = new int[]{0, 120, 85, 255, 210, 255, 110};
|
||||
public static final int[] blueHSV4 = new int[]{0, 130, 110, 255, 210, 255, 195};
|
||||
//蓝色 - 红色取反
|
||||
public static final int[] red2blueHSV = new int[]{0, 140, 110, 255, 150, 255, 110};
|
||||
//紫色
|
||||
public static final int[] purpleHSV = new int[]{0, 160, 125, 255, 150, 255, 110};
|
||||
public static final int[] purpleHSV1 = new int[]{0, 155, 125, 255, 150, 255, 110};
|
||||
public static final int[] purpleHSV2 = new int[]{0, 155, 125, 255, 125, 255, 110};
|
||||
public static final int[] purpleHSV3 = new int[]{0, 155, 125, 255, 125, 255, 200};
|
||||
//实验性 - 白
|
||||
public static final int[] whiteHSV = new int[]{0, 110, 0, 60, 0, 255, 225};
|
||||
//实验性 - 黑
|
||||
public static final int[] blackHSV = new int[]{0, 100, 47, 225, 50, 60, 0};
|
||||
|
||||
/* 车牌识别数据 */
|
||||
//浅蓝0、//黄色1、//品红2、//浅红色3、//蓝色4、//青色5、// 深红色6、//黑色7 车牌蓝底9 车牌绿底10
|
||||
public static double[][] PlateDetector_HSV_VALUE_LOW = {
|
||||
{10, 163, 147}, //浅蓝0
|
||||
{77, 163, 147}, //黄色1
|
||||
{146, 212, 140},//品红2
|
||||
{126, 155, 160},//浅红色3
|
||||
{0, 204, 178}, //蓝色4
|
||||
{35, 163, 147}, //青色5
|
||||
{110, 155, 160},//深红色6
|
||||
{0, 0, 0}, //黑色7
|
||||
{0, 0, 192}, //标准蓝8
|
||||
{0, 190, 190}, //车牌蓝底9 暗的TFT:0,190,190 亮的:0,180,190
|
||||
{22, 195, 158}, //车牌绿底10 暗的TFT H:21 S要调高一点:210 V:211 亮的TFT S值要调底一点:110 10,100,148
|
||||
{65, 0, 200}, //新能源车牌白变绿渐变
|
||||
};
|
||||
|
||||
public static double[][] PlateDetector_HSV_VALUE_HIGH = {
|
||||
{47, 255, 255}, //浅蓝0
|
||||
{111, 255, 255}, //黄色1
|
||||
{241, 255, 255.0}, //品红2
|
||||
{150, 255, 255}, //浅红色3
|
||||
{21, 255, 255}, //蓝色4
|
||||
{75, 255.0, 255}, //青色5
|
||||
{150, 255, 255}, //深红色6
|
||||
{180, 255, 120}, //黑色7
|
||||
{45, 238, 255}, //标准蓝8
|
||||
{28, 255, 255}, //车牌蓝底9 亮暗一样
|
||||
{73, 255, 255}, //车牌绿底10 暗H:66 亮H:83
|
||||
{110, 255, 255}, //新能源车牌白变绿渐变
|
||||
};
|
||||
|
||||
//浅蓝0、//黄色1、//品红2、//浅红色3、//蓝色4、//青色5、// 深红色6、//黑色7
|
||||
//暗 S、V=214,211 亮 S、V=176,160
|
||||
//浅蓝0、//黄色1、//品红2、//浅红色3、//蓝色4、//青色5、// 深红色6、//黑色7 车牌蓝底9 车牌绿底10
|
||||
public static double[][] HSV_VALUE_LOW = {
|
||||
{13, 176, 160},//浅蓝0 12,214,211
|
||||
{67, 176, 160},//黄色1
|
||||
{130, 176, 160},//品红2 暗:100, 176,160 亮:130,176,160
|
||||
{126, 176, 160},//浅红色3
|
||||
{0, 176, 160},//蓝色4
|
||||
{30, 176, 160},//青色5 35
|
||||
{103, 176, 160},// 深红色6
|
||||
{0, 0, 0},//黑色7 暗:0,187,0 亮:0,0,0
|
||||
{0, 0, 192},//标准蓝8
|
||||
{0, 150, 190},//车牌蓝底9 暗的TFT:0,190,190 亮的:0,180,190
|
||||
{22, 104, 161},//车牌绿底10 暗的TFT H:21 S要调高一点:210 V:211 亮的TFT S值要调底一点:110 10,100,148
|
||||
};
|
||||
|
||||
public static double[][] HSV_VALUE_HIGH = {
|
||||
{30, 255, 255},//浅蓝0
|
||||
{111, 255, 255},//黄色1
|
||||
{241, 255, 255.0},//品红2
|
||||
{150, 255, 255},//浅红色3
|
||||
{12, 255, 255},//蓝色4
|
||||
{70, 255.0, 255},//青色5 90
|
||||
{150, 255, 255},// 深红色6
|
||||
{255, 255, 150},//黑色7 暗:28,255,184 亮:255,255,150
|
||||
{45, 238, 255},//标准蓝8
|
||||
{126, 255, 255},//车牌蓝底9 亮暗一样
|
||||
{120, 255, 255},//车牌绿底10 暗H:66 亮H:83
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) https://github.com/gh-xiao/EmbeddedCar
|
||||
* This file is pull from GitHub open source project
|
||||
* Integrated by UnknownObject at 2023 - 6 - 7
|
||||
*/
|
||||
|
||||
package com.uns.maincar.open_source.utils;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* 形状统计
|
||||
*/
|
||||
public class ShapeStatistics
|
||||
{
|
||||
|
||||
//统计形状数量
|
||||
private HashMap<String, Integer> shapeStatistics = new HashMap<>();
|
||||
|
||||
/**
|
||||
* 获取指定形状的数量
|
||||
*
|
||||
* @param shapeName 三角形/矩形/菱形/五角星/圆形/总计
|
||||
* @return 数量
|
||||
*/
|
||||
public Integer getCounts(String shapeName)
|
||||
{
|
||||
return shapeStatistics.get(shapeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置形状的数量
|
||||
*
|
||||
* @param shapeStatistics 包含该形状数量的HashMap对象
|
||||
*/
|
||||
public void setShapeStatistics(HashMap<String, Integer> shapeStatistics)
|
||||
{
|
||||
this.shapeStatistics = shapeStatistics;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright (c) https://github.com/gh-xiao/EmbeddedCar
|
||||
* This file is pull from GitHub open source project
|
||||
* Integrated by UnknownObject at 2023 - 6 - 7
|
||||
*/
|
||||
|
||||
package com.uns.maincar.open_source.vehicle;
|
||||
|
||||
import android.content.res.AssetManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import org.tensorflow.lite.examples.detection.env.Logger;
|
||||
import org.tensorflow.lite.examples.detection.tflite.Classifier;
|
||||
import org.tensorflow.lite.examples.detection.tflite.DetectorFactory;
|
||||
import org.tensorflow.lite.examples.detection.tflite.YoloV5Classifier;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 使用基于YoloV5-tfLite模型的交通标志物识别
|
||||
*/
|
||||
public class YoloV5_tfLite_VIDDetector
|
||||
{
|
||||
|
||||
// Which detection model to use: by default uses Tensorflow Object Detection API frozen
|
||||
// checkpoints.
|
||||
// enum DetectorMode {TF_OD_API}
|
||||
|
||||
//日志对象
|
||||
private static final Logger LOGGER = new Logger();
|
||||
//枚举常量 - 检测模式
|
||||
// private static final DetectorMode MODE = DetectorMode.TF_OD_API;
|
||||
public static final float MINIMUM_CONFIDENCE_TF_OD_API = 0.3f;
|
||||
//最小置信度
|
||||
public static float minimumConfidence;
|
||||
//核心检测对象
|
||||
private YoloV5Classifier detector;
|
||||
//模型列表
|
||||
private final String[] models = new String[]{"VIDyolov5s-fp16.tflite", "VIDyolov5s-fp16-2.tflite"};
|
||||
//检测图片
|
||||
private Bitmap SaveBitmap;
|
||||
private long timestamp = 0;
|
||||
|
||||
public Bitmap getSaveBitmap()
|
||||
{
|
||||
return SaveBitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载模型配置
|
||||
*
|
||||
* @param device 使用何种设备加载模型
|
||||
* @param numThreads 使用多少线程加载
|
||||
* @param assetManager assetManager管理对象
|
||||
*/
|
||||
public boolean LoadModel(String device, int numThreads, AssetManager assetManager)
|
||||
{
|
||||
//模型文件
|
||||
String modelString = models[0];
|
||||
//检测类别(标签)
|
||||
String labelFilename = "VIDclass.txt";
|
||||
/* 线程数(不推荐超过9线程数) */
|
||||
if (numThreads > 9) numThreads = 4;
|
||||
LOGGER.i("Changing model to ***" + modelString + "*** device ***" + device + "***");
|
||||
|
||||
/* Try to load model. */
|
||||
/* 尝试加载模型 */
|
||||
try
|
||||
{
|
||||
detector = DetectorFactory.getDetector(assetManager, modelString, labelFilename);
|
||||
// Customize the interpreter to the type of device we want to use.
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
LOGGER.e(e, "Exception in updateActiveModel()");
|
||||
// Toast toast = Toast.makeText(FirstActivity.getContext(), "Classifier could not be initialized", Toast.LENGTH_SHORT);
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (device)
|
||||
{
|
||||
case "GPU":
|
||||
detector.useGpu();
|
||||
break;
|
||||
case "NNAPI":
|
||||
detector.useNNAPI();
|
||||
break;
|
||||
default:
|
||||
detector.useCPU();
|
||||
break;
|
||||
}
|
||||
/* 设置线程数 */
|
||||
detector.setNumThreads(numThreads);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测图片
|
||||
*
|
||||
* @param inputBitmap -
|
||||
* @return 检测结果
|
||||
*/
|
||||
public String processImage(Bitmap inputBitmap)
|
||||
{
|
||||
/* 结果列表对象 */
|
||||
List<Classifier.Recognition> recognitions = new LinkedList<>();
|
||||
/* 将结果转换成Gson */
|
||||
Gson gson = new Gson();
|
||||
|
||||
if (inputBitmap == null) return gson.toJson(recognitions);
|
||||
//416*416
|
||||
int cropSize = detector.getInputSize();
|
||||
System.out.println(cropSize);
|
||||
|
||||
int width = inputBitmap.getWidth();
|
||||
int height = inputBitmap.getHeight();
|
||||
float scaleWidth = ((float) cropSize) / width;
|
||||
float scaleHeight = ((float) cropSize) / height;
|
||||
//矩阵
|
||||
Matrix matrix = new Matrix();
|
||||
matrix.postScale(scaleWidth, scaleHeight);
|
||||
|
||||
/* 将输入图片通过矩阵变换得到416*416大小的新图片 */
|
||||
Bitmap croppedBitmap = Bitmap.createBitmap(inputBitmap, 0, 0, width, height, matrix, true);
|
||||
Bitmap draw = croppedBitmap.copy(Bitmap.Config.ARGB_8888, true);
|
||||
|
||||
++timestamp;
|
||||
final long currTimestamp = timestamp;
|
||||
|
||||
LOGGER.i("Preparing image " + currTimestamp + " for detection in bg thread.");
|
||||
/* 利用分类器classifier对图片进行预测分析,得到图片为每个分类的概率. 比较耗时 */
|
||||
LOGGER.i("Running detection on image " + currTimestamp);
|
||||
|
||||
final long startTime = SystemClock.uptimeMillis();
|
||||
/* 核心检测 */
|
||||
final List<Classifier.Recognition> results = detector.recognizeImage(croppedBitmap);
|
||||
/* 计算检测时间 */
|
||||
long lastProcessingTimeMs = SystemClock.uptimeMillis() - startTime;
|
||||
/* 检测出多少对象 */
|
||||
Log.e("CHECK", "run: " + results.size());
|
||||
/* 检测时间 */
|
||||
Log.i("Time Spent: ", lastProcessingTimeMs + "ms");
|
||||
|
||||
/* 筛选通过最低置信度阈值的识别结果 */
|
||||
final List<Classifier.Recognition> mappedRecognitions = new LinkedList<>();
|
||||
for (final Classifier.Recognition result : results)
|
||||
{
|
||||
final RectF location = result.getLocation();
|
||||
if (location != null && result.getConfidence() >= minimumConfidence)
|
||||
{
|
||||
result.setLocation(location);
|
||||
/* 将通过最低置信度的结果添加到新List */
|
||||
mappedRecognitions.add(result);
|
||||
//识别结果
|
||||
Log.e("result: ", result.getTitle() + result.getConfidence());
|
||||
drawBitmap(result, draw);
|
||||
}
|
||||
}
|
||||
|
||||
return gson.toJson(mappedRecognitions.size() > 0 ? mappedRecognitions : recognitions);
|
||||
// //最终结果
|
||||
// if (mappedRecognitions.size() != 0) {
|
||||
// /* 排列出最高置信度的结果 */
|
||||
// Collections.sort(mappedRecognitions, (o1, o2) -> (int) (o1.getConfidence() - o2.getConfidence()));
|
||||
// Log.e("SUCCESS", String.valueOf(mappedRecognitions.get(0).getConfidence()));
|
||||
// return mappedRecognitions.get(0).getTitle();
|
||||
// } else {
|
||||
// Log.e("ERROR", "识别错误");
|
||||
// return "car";
|
||||
// }
|
||||
}
|
||||
|
||||
private void drawBitmap(Classifier.Recognition result, Bitmap resultBitmap)
|
||||
{
|
||||
final Canvas canvas = new Canvas(resultBitmap);
|
||||
final Paint paint = new Paint();
|
||||
paint.setColor(Color.RED);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeWidth(2.0f);
|
||||
canvas.drawRect(result.getLocation(), paint);
|
||||
SaveBitmap = resultBitmap.copy(Bitmap.Config.ARGB_8888, true);
|
||||
}
|
||||
}
|
||||
@@ -185,4 +185,34 @@
|
||||
android:layout_weight="1"
|
||||
android:text="立即崩溃" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_os_shapecolor"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
android:layout_weight="1"
|
||||
android:text="开源\n形状颜色识别" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_os_trafficsign"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
android:layout_weight="1"
|
||||
android:text="开源\n交通标志识别" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_os_vehicle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
android:layout_weight="1"
|
||||
android:text="开源\n车型识别" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
Reference in New Issue
Block a user