add colored qr code

master
UnknownObject 2 years ago
parent dd8000a498
commit 892c19130c

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/app/src/main/cpp/colored_qr_detect/colored_qr_decode.cpp" charset="GBK" />
</component>
</project>

@ -165,6 +165,21 @@ add_library( # Sets the name of the library.
lib_hyper_lpr/src/SegmentationFreeRecognizer.cpp
lib_hyper_lpr/javaWarpper.cpp)
add_library( # Sets the name of the library.
colored_qr_code
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
color_reco.cpp
public_types.cpp
opencv_support.cpp
opencv_libqr/cv_qrcode.cpp
colored_qr_detect/QRCode.cpp
colored_qr_detect/QRCodeSet.cpp
colored_qr_detect/colored_qr_decode.cpp)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
@ -287,3 +302,12 @@ target_link_libraries( # Specifies the target library.
${OpenCV_LIBS}
${jnigraphics-lib}
${log-lib})
target_link_libraries( # Specifies the target library.
colored_qr_code
# Links the target library to the log library
# included in the NDK.
${OpenCV_LIBS}
${jnigraphics-lib}
${log-lib})

@ -16,13 +16,13 @@ namespace uns
}
//识别颜色,逐像素便利并统计出现次数最多的颜色作为最终结果
std::string ColorReco::RecoColor(const cv::Mat& img)
Colors::CVColor ColorReco::RecoColor(const cv::Mat &img)
{
int max_count = 0;
std::string color_name = "null";
Colors::CVColor max_color;
Colors::ColorCount counter = basic_color_counter;
if (img.empty())
return color_name;
return std_color_white;
for (int r = 0; r < img.rows; r++)
{
for (int c = 0; c < img.cols; c++)
@ -46,17 +46,42 @@ namespace uns
counter[std_color_white]++;
}
}
for (auto& ele : counter)
for (auto &ele: counter)
{
if ((ele.first == std_color_white)/* || (ele.first == std_color_yellow)*/)
if ((ele.first == std_color_white))
continue;
if (ele.second > max_count)
{
max_color = ele.first;
max_count = ele.second;
color_name = color_name_map.at(ele.first);
}
}
return color_name;
return max_color;
}
std::string ColorReco::RecoColorStr(const Colors::CVColor &color)
{
return color_name_map.at(color);
}
bool ColorReco::IsRed(const Colors::CVColor &result)
{
return (result == std_color_red);
}
bool ColorReco::IsGreen(const Colors::CVColor &result)
{
return (result == std_color_green);
}
bool ColorReco::IsBlue(const Colors::CVColor &result)
{
return (result == std_color_blue);
}
bool ColorReco::IsYellow(const Colors::CVColor &result)
{
return (result == std_color_yellow);
}
};

@ -44,19 +44,31 @@ namespace uns
};
std::map<Colors::CVColor, std::string> color_name_map =
{
{ std_color_red, "red" },
{ std_color_green, "green"},
{ std_color_blue, "blue"},
{ std_color_yellow, "yellow"},
{ std_color_purple, "purple"},
{ std_color_cyan, "cyan"},
{ std_color_black, "black"},
{ std_color_white, "white"},
{std_color_red, "red" },
{std_color_green, "green"},
{std_color_blue, "blue"},
{std_color_yellow, "yellow"},
{std_color_purple, "purple"},
{std_color_cyan, "cyan"},
{std_color_black, "black"},
{std_color_white, "white"},
};
private:
bool MuchLarger(int a, int b, double rate);
public:
std::string RecoColor(const cv::Mat& img);
Colors::CVColor RecoColor(const cv::Mat &img);
std::string RecoColorStr(const Colors::CVColor &color);
public:
bool IsRed(const Colors::CVColor &result);
bool IsGreen(const Colors::CVColor &result);
bool IsBlue(const Colors::CVColor &result);
bool IsYellow(const Colors::CVColor &result);
};
};

@ -0,0 +1,66 @@
#include "QRCode.h"
namespace uns
{
bool QRCode::IsSameRect(const cv::Rect &r1, const cv::Rect &r2) const
{
cv::Point c1 = r1.tl();
cv::Point c2 = r2.tl();
int x_offset = __ABS__(c1.x - c2.x);
int y_offset = __ABS__(c1.y - c2.y);
return ((x_offset <= EQUAL_OFFSET) && (y_offset <= EQUAL_OFFSET));
}
QRCode::QRCode()
{
color = Color::Invalidate;
}
QRCode::QRCode(const cv::Rect &r)
{
position = r;
color = Color::Invalidate;
}
QRCode::QRCode(const QRCode &obj)
{
color = obj.color;
position = obj.position;
}
QRCode::QRCode(Color c, const cv::Rect &r)
{
color = c;
position = r;
}
QRCode::Color QRCode::GetColor() const
{
return color;
}
cv::Rect QRCode::GetRect() const
{
return position;
}
void QRCode::SetColor(Color color)
{
this->color = color;
}
void QRCode::SetRect(const cv::Rect &rect)
{
position = rect;
}
bool QRCode::operator==(const QRCode &obj) const
{
return IsSameRect(position, obj.position);
}
bool QRCode::operator!=(const QRCode &obj) const
{
return (!IsSameRect(position, obj.position));
}
}

@ -0,0 +1,52 @@
#pragma once
#include <vector>
#include <opencv2/core/core.hpp>
#define __ABS__(x) (x < 0 ? -x : x)
namespace uns
{
class QRCode
{
public:
enum class Color
{
Red = 0,
Green = 1,
Yellow = 2,
Blue = 3,
Invalidate = -1
};
private:
Color color;
cv::Rect position;
const int EQUAL_OFFSET = 2;
private:
bool IsSameRect(const cv::Rect &r1, const cv::Rect &r2) const;
public:
QRCode();
QRCode(const cv::Rect &r);
QRCode(const QRCode &obj);
QRCode(Color c, const cv::Rect &r);
public:
Color GetColor() const;
cv::Rect GetRect() const;
void SetColor(Color color);
void SetRect(const cv::Rect &rect);
public:
bool operator==(const QRCode &obj) const;
bool operator!=(const QRCode &obj) const;
};
}

@ -0,0 +1,72 @@
#include "QRCodeSet.h"
namespace uns
{
QRCodeSet::QRCodeSet()
{
}
QRCodeSet::QRCodeSet(const QRCodeSet &obj)
{
qr_codes.clear();
qr_codes.shrink_to_fit();
current_index = obj.current_index;
for (auto &ele: obj.qr_codes)
qr_codes.push_back(ele);
}
void QRCodeSet::Clear()
{
qr_codes.clear();
current_index = 0;
}
size_t QRCodeSet::Size()
{
return qr_codes.size();
}
bool QRCodeSet::HasNextCode()
{
return (current_index < qr_codes.size());
}
void QRCodeSet::ResetPointer()
{
current_index = 0;
}
QRCode &QRCodeSet::GetNextCode()
{
return qr_codes[current_index++];
}
void QRCodeSet::Insert(const QRCode &obj, const cv::Mat &source_img)
{
for (auto &ele: qr_codes)
if (ele == obj)
return;
if (obj.GetColor() != QRCode::Color::Invalidate)
qr_codes.push_back(obj);
else
{
QRCode code(obj);
ColorReco color_reco;
Colors::CVColor result = color_reco.RecoColor(source_img(code.GetRect()));
if (color_reco.IsRed(result))
code.SetColor(QRCode::Color::Red);
else if (color_reco.IsGreen(result))
code.SetColor(QRCode::Color::Green);
else if (color_reco.IsBlue(result))
code.SetColor(QRCode::Color::Blue);
else if (color_reco.IsYellow(result))
code.SetColor(QRCode::Color::Yellow);
qr_codes.push_back(code);
}
}
QRCode &QRCodeSet::operator[](size_t index)
{
return qr_codes[index];
}
}

@ -0,0 +1,36 @@
#pragma once
#include <vector>
#include "QRCode.h"
#include "../color_reco.h"
namespace uns
{
class QRCodeSet
{
private:
int current_index = 0;
std::vector<QRCode> qr_codes;
public:
QRCodeSet();
QRCodeSet(const QRCodeSet &obj);
public:
void Clear();
size_t Size();
bool HasNextCode();
void ResetPointer();
QRCode &GetNextCode();
void Insert(const QRCode &obj, const cv::Mat &source_img);
public:
QRCode &operator[](size_t index);
};
}

@ -0,0 +1,253 @@
#include "colored_qr_decode.h"
namespace uns
{
SingleQRImage::SingleQRImage()
{
color = QRCode::Color::Invalidate;
}
SingleQRImage::SingleQRImage(const SingleQRImage &img)
{
color = img.color;
img.binary.copyTo(binary);
img.colored.copyTo(colored);
}
SingleQRImage::SingleQRImage(const cv::Mat &clr_src, const RGBChannel &rgb_src,
const QRCode &code)
{
clr_src(code.GetRect()).copyTo(colored);
switch (code.GetColor())
{
case QRCode::Color::Red:
rgb_src[1](code.GetRect()).copyTo(binary);
break;
case QRCode::Color::Green:
rgb_src[0](code.GetRect()).copyTo(binary);
break;
case QRCode::Color::Blue:
rgb_src[0](code.GetRect()).copyTo(binary);
break;
case QRCode::Color::Yellow:
rgb_src[2](code.GetRect()).copyTo(binary);
break;
default:
clr_src(code.GetRect()).copyTo(binary);
break;
}
color = code.GetColor();
}
bool SingleQRImage::DataValidate() const
{
return ((!binary.empty()) && (!colored.empty()));
}
QRCode::Color SingleQRImage::GetColor() const
{
return color;
}
cv::Mat SingleQRImage::GetBinaryImage() const
{
return binary;
}
cv::Mat SingleQRImage::GetColoredImage() const
{
return colored;
}
RGBChannel ColoredQrDecode::SplitRGBChannel(const cv::Mat &obj)
{
cv::Mat r(obj.size(), CV_8UC1), g(obj.size(), CV_8UC1), b(obj.size(), CV_8UC1);
for (int i = 0; i < obj.rows; i++)
{
for (int j = 0; j < obj.cols; j++)
{
cv::Vec3b pixel = obj.at<cv::Vec3b>(i, j);
r.at<uchar>(i, j) = pixel[2];
g.at<uchar>(i, j) = pixel[1];
b.at<uchar>(i, j) = pixel[0];
}
}
return RGBChannel({r, g, b});
}
//旋转图片45°
bool ColoredQrDecode::RotateImage(const cv::Mat &src, cv::Mat &dst, float angle)
{
if (src.empty())
return false;
double alpha = -angle * CV_PI / 180.0;//convert angle to radian format
cv::Point2f srcP[3];
cv::Point2f dstP[3];
srcP[0] = cv::Point2f(0, (float) src.rows);
srcP[1] = cv::Point2f((float) src.cols, 0);
srcP[2] = cv::Point2f((float) src.cols, (float) src.rows);
//rotate the pixels
for (int i = 0; i < 3; i++)
dstP[i] = cv::Point2f(srcP[i].x * cos(alpha) - srcP[i].y * sin(alpha),
srcP[i].y * cos(alpha) + srcP[i].x * sin(alpha));
double minx, miny, maxx, maxy;
minx = std::min(std::min(std::min(dstP[0].x, dstP[1].x), dstP[2].x), float(0.0));
miny = std::min(std::min(std::min(dstP[0].y, dstP[1].y), dstP[2].y), float(0.0));
maxx = std::max(std::max(std::max(dstP[0].x, dstP[1].x), dstP[2].x), float(0.0));
maxy = std::max(std::max(std::max(dstP[0].y, dstP[1].y), dstP[2].y), float(0.0));
int w = maxx - minx;
int h = maxy - miny;
//translation
for (int i = 0; i < 3; i++)
{
if (minx < 0)
dstP[i].x -= minx;
if (miny < 0)
dstP[i].y -= miny;
}
cv::Mat warpMat = cv::getAffineTransform(srcP, dstP);
cv::warpAffine(src, dst, warpMat, cv::Size(w, h), 1, 0,
cv::Scalar(255, 255, 255));//extend size
return true;
}
//使用导出的OpenCV库切分多个二维码
bool ColoredQrDecode::SplitMultipleQR(const cv::Mat &img, std::vector<cv::Rect> &rects)
{
uns_cv_export::QRCodeDetector qrcode;
std::vector<cv::Point> corners;
if (!qrcode.detectMulti(img, corners))
return false;
if (!corners.empty())
{
if ((corners.size() % 4) != 0)
return false;
for (size_t i = 0; i < corners.size(); i += 4)
rects.push_back(cv::boundingRect(
std::vector<cv::Point>(corners.begin() + i, corners.begin() + i + 4)));
return true;
}
else
return false;
}
//检查是否有下一个二维码图片
bool ColoredQrDecode::HasNextImage()
{
return qr_codes.HasNextCode();
}
//获取下一个二维码图片
SingleQRImage ColoredQrDecode::GetNextImage()
{
return SingleQRImage(source_image, split_channel_image, qr_codes.GetNextCode());
}
//获取指定颜色的二维码
SingleQRImage ColoredQrDecode::GetCodeByColor(QRCode::Color color)
{
qr_codes.ResetPointer();
while (qr_codes.HasNextCode())
{
QRCode code = qr_codes.GetNextCode();
if (code.GetColor() == color)
{
qr_codes.ResetPointer();
return SingleQRImage(source_image, split_channel_image, code);
}
}
qr_codes.ResetPointer();
return SingleQRImage();
}
//切分并存储二维码
bool ColoredQrDecode::SplitQR(const cv::Mat &img, bool rotate_45)
{
std::vector<cv::Rect> rects;
if (rotate_45)
RotateImage(img, source_image, 45.00f);
else
img.copyTo(source_image);
qr_codes.Clear();
split_channel_image = SplitRGBChannel(source_image);
SplitMultipleQR(split_channel_image[0], rects);
SplitMultipleQR(split_channel_image[1], rects);
SplitMultipleQR(split_channel_image[2], rects);
for (const auto &ele: rects)
qr_codes.Insert(QRCode(ele), source_image);
return (qr_codes.Size() != 0);
}
//清空存储
void ColoredQrDecode::Clear()
{
qr_codes.Clear();
}
};
uns::ColoredQrDecode global_colored_qr_decoder;
uns::QRCode::Color global_current_qr_color = uns::QRCode::Color::Invalidate;
//导出的图像处理函数
extern "C" JNIEXPORT
jboolean JNICALL
Java_com_uns_maincar_cpp_1interface_ColoredQRDecoder_ProcessColoredQR(JNIEnv *env, jclass _this,
jobject image,
jboolean rotate)
{
cv::Mat img_input;
if (!BitmapToMat(env, image, img_input))
return false;
global_colored_qr_decoder.Clear();
return global_colored_qr_decoder.SplitQR(img_input, rotate);
}
//导出的检查是否有下一个二维码函数
extern "C" JNIEXPORT
jboolean JNICALL
Java_com_uns_maincar_cpp_1interface_ColoredQRDecoder_HasNextQR(JNIEnv *env, jclass _this)
{
return global_colored_qr_decoder.HasNextImage();
}
//导出的获取下一个二维码图片函数
extern "C" JNIEXPORT
jobject JNICALL
Java_com_uns_maincar_cpp_1interface_ColoredQRDecoder_GetNextQR(JNIEnv *env, jclass _this)
{
uns::SingleQRImage qr_img = global_colored_qr_decoder.GetNextImage();
global_current_qr_color = qr_img.GetColor();
cv::Mat next = qr_img.GetBinaryImage();
jobject bmp = GenerateBitmap(env, next.cols, next.rows);
MatToBitmap(env, next, bmp);
return bmp;
}
//导出的获取当前返回的二维码颜色函数
extern "C" JNIEXPORT
jint JNICALL
Java_com_uns_maincar_cpp_1interface_ColoredQRDecoder_GetCurrentQRColor(JNIEnv *env, jclass _this)
{
return (jint) global_current_qr_color;
}
//导出的根据指定颜色获取二维码函数
extern "C" JNIEXPORT
jobject JNICALL
Java_com_uns_maincar_cpp_1interface_ColoredQRDecoder_GetSpecColorQR(JNIEnv *env, jclass _this,
jint color)
{
cv::Mat next = global_colored_qr_decoder.GetCodeByColor(
(uns::QRCode::Color) color).GetBinaryImage();
jobject bmp = GenerateBitmap(env, next.cols, next.rows);
MatToBitmap(env, next, bmp);
return bmp;
}
//导出的强制清空函数
extern "C" JNIEXPORT
void JNICALL
Java_com_uns_maincar_cpp_1interface_ColoredQRDecoder_ForceClear(JNIEnv *env, jclass _this)
{
global_colored_qr_decoder.Clear();
}

@ -0,0 +1,61 @@
#include <array>
#include <jni.h>
#include <vector>
#include "QRCodeSet.h"
#include "../opencv_support.h"
#include <opencv2/core/core.hpp>
#include "../opencv_libqr/cv_qrcode.h"
#include <opencv2/imgproc/imgproc.hpp>
namespace uns
{
using RGBChannel = std::array<cv::Mat, 3>;
class SingleQRImage
{
private:
cv::Mat binary;
cv::Mat colored;
QRCode::Color color;
public:
SingleQRImage();
SingleQRImage(const SingleQRImage &img);
SingleQRImage(const cv::Mat &clr_src, const RGBChannel &rgb_src, const QRCode &code);
public:
bool DataValidate() const;
QRCode::Color GetColor() const;
cv::Mat GetBinaryImage() const;
cv::Mat GetColoredImage() const;
};
class ColoredQrDecode
{
private:
QRCodeSet qr_codes;
cv::Mat source_image;
RGBChannel split_channel_image;
private:
RGBChannel SplitRGBChannel(const cv::Mat &obj);
bool RotateImage(const cv::Mat &src, cv::Mat &dst, float angle);
bool SplitMultipleQR(const cv::Mat &img, std::vector<cv::Rect> &rects);
public:
void Clear();
bool HasNextImage();
SingleQRImage GetNextImage();
SingleQRImage GetCodeByColor(QRCode::Color color);
bool SplitQR(const cv::Mat &img, bool rotate_45 = false);
};
};

@ -61,6 +61,17 @@ public class Commands
public static byte OCR_TEXT_DATA = (byte) 0xD9;
public static byte OCR_TEXT_FINISH = (byte) 0xE9;
//车型
public static byte VEHICLE_SUCCESS = (byte) 0xAA;
public static byte VEHICLE_FAILURE = (byte) 0xBA;
public static byte VEHICLE_TYPE_BIKE = (byte) 0x0A;
public static byte VEHICLE_TYPE_MOTOR = (byte) 0x0B;
public static byte VEHICLE_TYPE_CAR = (byte) 0x0C;
public static byte VEHICLE_TYPE_TRUCK = (byte) 0x0D;
public static byte VEHICLE_TYPE_VAN = (byte) 0x0E;
public static byte VEHICLE_TYPE_BUS = (byte) 0x0F;
//运行指定任务
public static byte RUN_SINGE_TASK = (byte) 0xA0;
public static byte TASK_NUMBER_0 = 0x00;
@ -98,4 +109,5 @@ public class Commands
public static final byte RECEIVE_TRAFFIC_SIGN = (byte) 0xA6;
public static final byte RECEIVE_TEXT_OCR = (byte) 0xA7;
public static final byte RECEIVE_OCR_DATA_OK = (byte) 0xB7;
public static final byte RECEIVE_VEHICLE = (byte) 0xA8;
}

@ -0,0 +1,125 @@
/*
* Copyright (c) 2023. UnknownNetworkService Group
* This file is created by UnknownObject at 2023 - 6 - 16
*/
package com.uns.maincar.cpp_interface;
import static com.uns.maincar.cpp_interface.QRDecoder.ERR_DECODE_FAILED;
import static com.uns.maincar.cpp_interface.QRDecoder.ERR_IMAGE_GENERATOR_FAILED;
import static com.uns.maincar.cpp_interface.QRDecoder.ERR_NATIVE_LIBRARY_FAILED;
import static com.uns.maincar.cpp_interface.QRDecoder.ERR_NO_CODE_DETECTED;
import static com.uns.maincar.cpp_interface.QRDecoder.ERR_NO_MORE_CODE;
import android.graphics.Bitmap;
import com.uns.maincar.constants.GlobalColor;
import com.zxingcpp.BarcodeReader;
public class ColoredQRDecoder
{
static
{
System.loadLibrary("colored_qr_code");
}
public static final String ERR_COLOR_INVALIDATE = "QR: Invalid Color Parameter.";
private static native void ForceClear();
private static native Bitmap GetNextQR();
private static native boolean HasNextQR();
private static native int GetCurrentQRColor();
private static native Bitmap GetSpecColorQR(int color);
private static native boolean ProcessColoredQR(Bitmap image, boolean rotate);
public static boolean BeginQRDecode(Bitmap img, boolean rotate)
{
return ProcessColoredQR(img, rotate);
}
public static boolean HasNextCode()
{
return HasNextQR();
}
public static String DecodeNextQR()
{
BarcodeReader reader = new BarcodeReader();
if (HasNextQR())
{
Bitmap bmp = GetNextQR();
if (bmp == null)
return ERR_IMAGE_GENERATOR_FAILED;
BarcodeReader.Result result = reader.read(bmp, QRDecoder.GetRect(bmp), 0);
if (result != null)
return result.getText();
else
return ERR_DECODE_FAILED;
}
else
return ERR_NO_MORE_CODE;
}
public static GlobalColor GetThisQRColor()
{
switch (GetCurrentQRColor())
{
case 0:
return GlobalColor.RED;
case 1:
return GlobalColor.GREEN;
case 2:
return GlobalColor.YELLOW;
case 3:
return GlobalColor.BLUE;
}
return GlobalColor.INVALIDATE;
}
public static String DecodeSpecColorQR(GlobalColor color)
{
int i_color = -1;
switch (color)
{
case RED:
i_color = 0;
break;
case GREEN:
i_color = 1;
break;
case YELLOW:
i_color = 2;
break;
case BLUE:
i_color = 3;
break;
}
if (i_color <= -1)
return ERR_COLOR_INVALIDATE;
BarcodeReader reader = new BarcodeReader();
Bitmap bmp = GetSpecColorQR(i_color);
if (bmp == null)
return ERR_IMAGE_GENERATOR_FAILED;
BarcodeReader.Result result = reader.read(bmp, QRDecoder.GetRect(bmp), 0);
if (result != null)
return result.getText();
else
return ERR_DECODE_FAILED;
}
public static String SelfTest(Bitmap img)
{
if (!ProcessColoredQR(img, false))
return ERR_NATIVE_LIBRARY_FAILED;
if (!HasNextQR())
return ERR_NO_CODE_DETECTED;
String result = DecodeNextQR();
ForceClear();
return result;
}
}

@ -29,9 +29,9 @@ public class QRDecoder
private static native Bitmap GetNextQR();
private static native boolean ProcessQR(Bitmap image);
private static Rect GetRect(Bitmap img)
protected static Rect GetRect(Bitmap img)
{
return new Rect(0,0,img.getWidth(),img.getHeight());
return new Rect(0, 0, img.getWidth(), img.getHeight());
}
public static boolean BeginQRDecode(Bitmap img)

@ -15,6 +15,8 @@ import androidx.appcompat.app.AppCompatActivity;
import com.uns.maincar.R;
import com.uns.maincar.constants.Flags;
import java.util.Objects;
public class DebugPage extends AppCompatActivity
{
@ -27,6 +29,8 @@ public class DebugPage extends AppCompatActivity
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_debug_page);
Objects.requireNonNull(getSupportActionBar()).setTitle("主车远端服务程序 - 调试");
if (Parent == null)
{
Toast.makeText(this, "Context is null!", Toast.LENGTH_SHORT).show();

@ -5,6 +5,10 @@
package com.uns.maincar.gui;
import static com.uns.maincar.cpp_interface.ColoredQRDecoder.ERR_COLOR_INVALIDATE;
import static com.uns.maincar.cpp_interface.QRDecoder.ERR_DECODE_FAILED;
import static com.uns.maincar.cpp_interface.QRDecoder.ERR_IMAGE_GENERATOR_FAILED;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
@ -36,15 +40,14 @@ import com.uns.maincar.communication.WifiTransferCore;
import com.uns.maincar.constants.Commands;
import com.uns.maincar.constants.Flags;
import com.uns.maincar.constants.Flags.TrafficLightColors;
import com.uns.maincar.constants.GlobalSignType;
import com.uns.maincar.constants.GlobalColor;
import com.uns.maincar.cpp_interface.CarLicense;
import com.uns.maincar.cpp_interface.ColoredQRDecoder;
import com.uns.maincar.cpp_interface.EnvTest;
import com.uns.maincar.cpp_interface.MainCarAES;
import com.uns.maincar.cpp_interface.OCR;
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;
@ -173,7 +176,8 @@ public class MainActivity extends AppCompatActivity
break;
//收到QR指令开始识别二维码回传识别成功的数据
case Commands.RECEIVE_QR:
byte[] cmd = RecognizeQrCode();
//默认情况下使用黑白二维码识别
byte[] cmd = RecognizeQrCode(false, GlobalColor.INVALIDATE);
if (cmd[2] == Commands.QR_FAILED)
dtc_client.Send(cmd);
else
@ -204,7 +208,9 @@ public class MainActivity extends AppCompatActivity
case Commands.RECEIVE_TEXT_OCR:
OCRRecognizeText();
break;
//收到未知指令,回传异常指令,表示无法解析当前指令
case Commands.RECEIVE_VEHICLE:
dtc_client.Send(RecognizeVehicle());
//收到未知指令,回传异常指令,表示无法解析当前指令
default:
CommandEncoder error = new CommandEncoder();
dtc_client.Send(error.GenerateCommand(Commands.CMD_NOT_MATCH, (byte) 0x00, (byte) 0x00, (byte) 0x00));
@ -249,6 +255,16 @@ public class MainActivity extends AppCompatActivity
return MainCarAES.CalcAES(validate_result);
}
//处理彩色二维码数据使用从C++代码中导出的算法
private byte[] ProcessColoredQRData(String qr_data)
{
CommandEncoder encoder = new CommandEncoder();
if (qr_data.equals(""))
return encoder.GenerateCommand(Commands.QR_FAILED, (byte) 0, (byte) 0, (byte) 0);
else
return MainCarAES.CalcAES(qr_data);
}
//获取程序自检指令,根据自检状态返回成功或失败
private byte[] SystemStatusCommand()
{
@ -260,20 +276,39 @@ public class MainActivity extends AppCompatActivity
}
//识别二维码
private byte[] RecognizeQrCode()
private byte[] RecognizeQrCode(boolean colored, GlobalColor target_color)
{
CommandEncoder encoder = new CommandEncoder();
ArrayList<String> qr_result = new ArrayList<>();
if (!QRDecoder.BeginQRDecode(currImage))
return encoder.GenerateCommand(Commands.QR_FAILED, (byte) 0, (byte) 0, (byte) 0);
while (QRDecoder.HasNextCode())
qr_result.add(QRDecoder.DecodeNextQR());
if (qr_result.size() <= 0)
return encoder.GenerateCommand(Commands.QR_FAILED, (byte) 0, (byte) 0, (byte) 0);
if (!colored)
{
ArrayList<String> qr_result = new ArrayList<>();
if (!QRDecoder.BeginQRDecode(currImage))
return encoder.GenerateCommand(Commands.QR_FAILED, (byte) 0, (byte) 0, (byte) 0);
while (QRDecoder.HasNextCode())
qr_result.add(QRDecoder.DecodeNextQR());
if (qr_result.size() <= 0)
return encoder.GenerateCommand(Commands.QR_FAILED, (byte) 0, (byte) 0, (byte) 0);
else
{
qr_result.forEach(qr -> ToastLog(qr, false, true));
return ProcessQRData(qr_result);
}
}
else
{
qr_result.forEach(qr -> ToastLog(qr, false, true));
return ProcessQRData(qr_result);
boolean success = ColoredQRDecoder.BeginQRDecode(currImage, false);
if (!success)
success = ColoredQRDecoder.BeginQRDecode(currImage, true);
if (!success)
return encoder.GenerateCommand(Commands.QR_FAILED, (byte) 0, (byte) 0, (byte) 0);
String result = ColoredQRDecoder.DecodeSpecColorQR(target_color);
if (Objects.equals(result, ERR_COLOR_INVALIDATE) || Objects.equals(result, ERR_IMAGE_GENERATOR_FAILED) || Objects.equals(result, ERR_DECODE_FAILED))
return encoder.GenerateCommand(Commands.QR_FAILED, (byte) 0, (byte) 0, (byte) 0);
else
{
ToastLog(target_color + ":" + result, false, true);
return ProcessColoredQRData(result);
}
}
}
@ -300,54 +335,20 @@ public class MainActivity extends AppCompatActivity
private byte[] RecognizeShapeColor()
{
CommandEncoder encoder = new CommandEncoder();
if (!ShapeColor.RecognizeEverything(currImage))
/*if (!ShapeColor.RecognizeEverything(currImage))
return encoder.GenerateCommand(Commands.COLOR_SHAPE_FAILED, (byte) 0, (byte) 0, (byte) 0);
else
{
byte a = 0, b = 0, c = 0;
//测试用输出
/*ToastLog("C-R-"+ShapeColor.LookupResult(GlobalShape.CIRCLE, GlobalColor.RED), false, false);
ToastLog("C-BL-"+ShapeColor.LookupResult(GlobalShape.CIRCLE, GlobalColor.BLACK), false, false);
ToastLog("C-G-"+ShapeColor.LookupResult(GlobalShape.CIRCLE, GlobalColor.GREEN), false, false);
ToastLog("C-BU-"+ShapeColor.LookupResult(GlobalShape.CIRCLE, GlobalColor.BLUE), false, false);
ToastLog("C-C-"+ShapeColor.LookupResult(GlobalShape.CIRCLE, GlobalColor.CYAN), false, false);
ToastLog("C-P-"+ShapeColor.LookupResult(GlobalShape.CIRCLE, GlobalColor.PURPLE), false, false);
ToastLog("C-W-"+ShapeColor.LookupResult(GlobalShape.CIRCLE, GlobalColor.WHITE), false, false);
ToastLog("C-Y-"+ShapeColor.LookupResult(GlobalShape.CIRCLE, GlobalColor.YELLOW), false, false);
ToastLog("S-R-"+ShapeColor.LookupResult(GlobalShape.STAR, GlobalColor.RED), false, false);
ToastLog("S-BL-"+ShapeColor.LookupResult(GlobalShape.STAR, GlobalColor.BLACK), false, false);
ToastLog("S-G-"+ShapeColor.LookupResult(GlobalShape.STAR, GlobalColor.GREEN), false, false);
ToastLog("S-BU-"+ShapeColor.LookupResult(GlobalShape.STAR, GlobalColor.BLUE), false, false);
ToastLog("S-C-"+ShapeColor.LookupResult(GlobalShape.STAR, GlobalColor.CYAN), false, false);
ToastLog("S-P-"+ShapeColor.LookupResult(GlobalShape.STAR, GlobalColor.PURPLE), false, false);
ToastLog("S-W-"+ShapeColor.LookupResult(GlobalShape.STAR, GlobalColor.WHITE), false, false);
ToastLog("S-Y-"+ShapeColor.LookupResult(GlobalShape.STAR, GlobalColor.YELLOW), false, false);
ToastLog("s-R-"+ShapeColor.LookupResult(GlobalShape.SQUARE, GlobalColor.RED), false, false);
ToastLog("s-BL-"+ShapeColor.LookupResult(GlobalShape.SQUARE, GlobalColor.BLACK), false, false);
ToastLog("s-G-"+ShapeColor.LookupResult(GlobalShape.SQUARE, GlobalColor.GREEN), false, false);
ToastLog("s-BU-"+ShapeColor.LookupResult(GlobalShape.SQUARE, GlobalColor.BLUE), false, false);
ToastLog("s-C-"+ShapeColor.LookupResult(GlobalShape.SQUARE, GlobalColor.CYAN), false, false);
ToastLog("s-P-"+ShapeColor.LookupResult(GlobalShape.SQUARE, GlobalColor.PURPLE), false, false);
ToastLog("s-W-"+ShapeColor.LookupResult(GlobalShape.SQUARE, GlobalColor.WHITE), false, false);
ToastLog("s-Y-"+ShapeColor.LookupResult(GlobalShape.SQUARE, GlobalColor.YELLOW), false, false);
ToastLog("R-R-"+ShapeColor.LookupResult(GlobalShape.RECTANGLE, GlobalColor.RED), false, false);
ToastLog("R-BL-"+ShapeColor.LookupResult(GlobalShape.RECTANGLE, GlobalColor.BLACK), false, false);
ToastLog("R-G-"+ShapeColor.LookupResult(GlobalShape.RECTANGLE, GlobalColor.GREEN), false, false);
ToastLog("R-BU-"+ShapeColor.LookupResult(GlobalShape.RECTANGLE, GlobalColor.BLUE), false, false);
ToastLog("R-C-"+ShapeColor.LookupResult(GlobalShape.RECTANGLE, GlobalColor.CYAN), false, false);
ToastLog("R-P-"+ShapeColor.LookupResult(GlobalShape.RECTANGLE, GlobalColor.PURPLE), false, false);
ToastLog("R-W-"+ShapeColor.LookupResult(GlobalShape.RECTANGLE, GlobalColor.WHITE), false, false);
ToastLog("R-Y-"+ShapeColor.LookupResult(GlobalShape.RECTANGLE, GlobalColor.YELLOW), false, false);
ToastLog("T-R-"+ShapeColor.LookupResult(GlobalShape.TRIANGLE, GlobalColor.RED), false, false);
ToastLog("T-BL-"+ShapeColor.LookupResult(GlobalShape.TRIANGLE, GlobalColor.BLACK), false, false);
ToastLog("T-G-"+ShapeColor.LookupResult(GlobalShape.TRIANGLE, GlobalColor.GREEN), false, false);
ToastLog("T-BU-"+ShapeColor.LookupResult(GlobalShape.TRIANGLE, GlobalColor.BLUE), false, false);
ToastLog("T-C-"+ShapeColor.LookupResult(GlobalShape.TRIANGLE, GlobalColor.CYAN), false, false);
ToastLog("T-P-"+ShapeColor.LookupResult(GlobalShape.TRIANGLE, GlobalColor.PURPLE), false, false);
ToastLog("T-W-"+ShapeColor.LookupResult(GlobalShape.TRIANGLE, GlobalColor.WHITE), false, false);
ToastLog("T-Y-"+ShapeColor.LookupResult(GlobalShape.TRIANGLE, GlobalColor.YELLOW), false, false);*/
return encoder.GenerateCommand(Commands.COLOR_SHAPE_SUCCESS, a, b, c);
}
}*/
//形状颜色识别改用开源AI模型
ShapeDetector detector = new ShapeDetector();
detector.shapePicProcess(currImage);
ShapeColorResult result = detector.GetAllResult();
ToastLog(result.toString(), false, true);
return encoder.GenerateCommand();
}
//识别车牌
@ -401,7 +402,7 @@ public class MainActivity extends AppCompatActivity
private byte[] RecognizeTrafficSign()
{
CommandEncoder encoder = new CommandEncoder();
GlobalSignType type = TrafficSign.SignRecognize(currImage);
/*GlobalSignType type = TrafficSign.SignRecognize(currImage);
if (type == GlobalSignType.Failure)
return encoder.GenerateCommand(Commands.TRAFFIC_SIGN_FAILED, (byte) 0, (byte) 0, (byte) 0);
else
@ -429,7 +430,11 @@ public class MainActivity extends AppCompatActivity
break;
}
return encoder.GenerateCommand();
}
}*/
//交通标志改用开源AI模型识别
String result = TS_Detector.processImage(currImage);
ToastLog("Traffic Sign: " + result, false, true);
return encoder.GenerateCommand();
}
//识别静态文本
@ -468,6 +473,16 @@ public class MainActivity extends AppCompatActivity
}
}
//识别车型
private byte[] RecognizeVehicle()
{
CommandEncoder encoder = new CommandEncoder();
//使用开源AI模型识别车型
String result = VID_Detector.processImage(currImage);
ToastLog("Vehicle: " + result, false, true);
return encoder.GenerateCommand();
}
//数组转字符串,仅用于调试输出
private String ByteArray2String(byte[] arr)
{
@ -570,19 +585,47 @@ public class MainActivity extends AppCompatActivity
context.findViewById(R.id.btn_start_qr).setOnClickListener(view ->
{
ToastLog("QR Code Started", false, false);
ToastLog("QR Result: " + ByteArray2String(RecognizeQrCode()), false, false);
//默认情况下使用黑白二维码识别
ToastLog("QR Result: " + ByteArray2String(RecognizeQrCode(false, GlobalColor.INVALIDATE)), false, false);
context.finish();
});
context.findViewById(R.id.btn_start_red_qr).setOnClickListener(view ->
{
ToastLog("QR Code Started (Red)", false, false);
//针对彩色二维码单独测试二维码识别
ToastLog("QR (Red) Result: " + ByteArray2String(RecognizeQrCode(true, GlobalColor.RED)), false, false);
context.finish();
});
context.findViewById(R.id.btn_start_green_qr).setOnClickListener(view ->
{
ToastLog("QR Code Started (Green)", false, false);
//针对彩色二维码单独测试二维码识别
ToastLog("QR (Green) Result: " + ByteArray2String(RecognizeQrCode(true, GlobalColor.GREEN)), false, false);
context.finish();
});
context.findViewById(R.id.btn_start_yellow_qr).setOnClickListener(view ->
{
ToastLog("QR Code Started (Yellow)", false, false);
//针对彩色二维码单独测试二维码识别
ToastLog("QR (Yellow) Result: " + ByteArray2String(RecognizeQrCode(true, GlobalColor.YELLOW)), false, false);
context.finish();
});
context.findViewById(R.id.btn_start_light).setOnClickListener(view ->
{
ToastLog("Traffic Light Started", false, false);
ToastLog("TL Result: " + ByteArray2String(RecognizeTrafficLight()), false, false);
context.finish();
});
context.findViewById(R.id.btn_start_color_shape).setOnClickListener(view ->
{
ToastLog("Color Shape Started", false, false);
ToastLog("CS Result: " + ByteArray2String(RecognizeShapeColor()), false, false);
context.finish();
});
context.findViewById(R.id.btn_start_car_id).setOnClickListener(view ->
@ -591,12 +634,14 @@ public class MainActivity extends AppCompatActivity
Thread th_debug = new Thread(this::RecognizeCarID);
th_debug.start();
ToastLog("CID Finished", false, false);
context.finish();
});
context.findViewById(R.id.btn_start_sign).setOnClickListener(view ->
{
ToastLog("Traffic Sign Started", false, false);
ToastLog("TS Result: " + ByteArray2String(RecognizeTrafficSign()), false, false);
context.finish();
});
context.findViewById(R.id.btn_start_ocr).setOnClickListener(view ->
@ -604,6 +649,7 @@ public class MainActivity extends AppCompatActivity
ToastLog("OCR Started", false, false);
Thread th_debug = new Thread(this::OCRRecognizeText);
th_debug.start();
context.finish();
});
context.findViewById(R.id.btn_tft_page_down).setOnClickListener(view ->
@ -611,6 +657,7 @@ public class MainActivity extends AppCompatActivity
CommandEncoder encoder = new CommandEncoder();
dtc_client.ThreadSend(encoder.GenerateCommand(Commands.TFT_PAGE_DOWN, (byte) 0, (byte) 0, (byte) 0));
ToastLog("TFT Page Down Command Send.", false, true);
context.finish();
});
context.findViewById(R.id.btn_movement_control).setOnClickListener(view -> startActivity(new Intent(this, MovementController.class)));
@ -629,18 +676,21 @@ public class MainActivity extends AppCompatActivity
detector.shapePicProcess(currImage);
ShapeColorResult result = detector.GetAllResult();
ToastLog(result.toString(), false, false);
context.finish();
});
context.findViewById(R.id.btn_os_trafficsign).setOnClickListener(view ->
{
String res = TS_Detector.processImage(currImage);
ToastLog("Traffic Sign Result: " + res, false, false);
context.finish();
});
context.findViewById(R.id.btn_os_vehicle).setOnClickListener(view ->
{
String res = TS_Detector.processImage(currImage);
ToastLog("Vehicle Result: " + res, false, false);
context.finish();
});
}
//----------------------------------------到此处终止----------------------------------------

@ -22,6 +22,8 @@ import androidx.core.content.ContextCompat;
import com.uns.maincar.R;
import java.util.Objects;
//Android的存储权限的获取
public class PermissionGetter extends AppCompatActivity
{
@ -72,6 +74,8 @@ public class PermissionGetter extends AppCompatActivity
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_permission_getter);
Objects.requireNonNull(getSupportActionBar()).setTitle("主车远端服务程序 - 权限获取");
GetExternalStoragePrivilege();
}

@ -16,6 +16,8 @@ import com.uns.maincar.R;
import com.uns.maincar.constants.Commands;
import com.uns.maincar.constants.Flags;
import java.util.Objects;
public class RaceTasks extends AppCompatActivity
{
@ -49,6 +51,8 @@ public class RaceTasks extends AppCompatActivity
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_race_tasks);
Objects.requireNonNull(getSupportActionBar()).setTitle("主车远端服务程序 - 比赛任务");
if (Parent == null)
{
Toast.makeText(this, "Context is null!", Toast.LENGTH_SHORT).show();

@ -15,6 +15,8 @@ import androidx.appcompat.app.AppCompatActivity;
import com.uns.maincar.R;
import com.uns.maincar.constants.Flags;
import java.util.Objects;
public class SingleFunctionTest extends AppCompatActivity
{
@ -27,6 +29,8 @@ public class SingleFunctionTest extends AppCompatActivity
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_single_function_test);
Objects.requireNonNull(getSupportActionBar()).setTitle("主车远端服务程序 - 独立测试");
if (Parent == null)
{
Toast.makeText(this, "Context is null!", Toast.LENGTH_SHORT).show();

@ -215,4 +215,34 @@
android:layout_weight="1"
android:text="开源\n车型识别" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<Button
android:id="@+id/btn_start_red_qr"
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_start_green_qr"
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_start_yellow_qr"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="3dp"
android:layout_weight="1"
android:text="黄色\n二维码识别" />
</LinearLayout>
</LinearLayout>

@ -11,7 +11,7 @@
"fieldsDescending": {}
},
"memoizedSize": -1,
"memoizedHashCode": -124854207
"memoizedHashCode": 816914465
},
{
"level_": 0,
@ -25,7 +25,7 @@
"fieldsDescending": {}
},
"memoizedSize": -1,
"memoizedHashCode": 949559604
"memoizedHashCode": 1891328276
},
{
"level_": 0,
@ -39,6 +39,6 @@
"fieldsDescending": {}
},
"memoizedSize": -1,
"memoizedHashCode": -1251864759
"memoizedHashCode": -310096087
}
]

@ -11,7 +11,7 @@
"fieldsDescending": {}
},
"memoizedSize": -1,
"memoizedHashCode": -1065851171
"memoizedHashCode": -124082499
},
{
"level_": 0,
@ -25,7 +25,7 @@
"fieldsDescending": {}
},
"memoizedSize": -1,
"memoizedHashCode": 176408592
"memoizedHashCode": 1118177264
},
{
"level_": 0,
@ -39,6 +39,6 @@
"fieldsDescending": {}
},
"memoizedSize": -1,
"memoizedHashCode": -139244825
"memoizedHashCode": 802523847
}
]

@ -1,2 +1,7 @@
# C/C++ build system timings
# C/C++ build system timings
generate_cxx_metadata
create-invalidation-state 12ms
generate_cxx_metadata completed in 16ms

@ -198,3 +198,8 @@ generate_cxx_metadata completed in 26ms
# C/C++ build system timings
# C/C++ build system timings
generate_cxx_metadata
create-invalidation-state 24ms
generate_cxx_metadata completed in 29ms

@ -11,7 +11,7 @@
"fieldsDescending": {}
},
"memoizedSize": -1,
"memoizedHashCode": 1119238981
"memoizedHashCode": 2061007653
},
{
"level_": 0,
@ -25,7 +25,7 @@
"fieldsDescending": {}
},
"memoizedSize": -1,
"memoizedHashCode": -1243038600
"memoizedHashCode": -301269928
},
{
"level_": 0,
@ -39,6 +39,6 @@
"fieldsDescending": {}
},
"memoizedSize": -1,
"memoizedHashCode": -444239237
"memoizedHashCode": 497529435
}
]

@ -11,7 +11,7 @@
"fieldsDescending": {}
},
"memoizedSize": -1,
"memoizedHashCode": 1041076311
"memoizedHashCode": 1982844983
},
{
"level_": 0,
@ -25,7 +25,7 @@
"fieldsDescending": {}
},
"memoizedSize": -1,
"memoizedHashCode": 286425208
"memoizedHashCode": 1228193880
},
{
"level_": 0,
@ -39,6 +39,6 @@
"fieldsDescending": {}
},
"memoizedSize": -1,
"memoizedHashCode": 704135946
"memoizedHashCode": 1645904618
}
]

@ -11,7 +11,7 @@
"fieldsDescending": {}
},
"memoizedSize": -1,
"memoizedHashCode": 783740077
"memoizedHashCode": 2049212457
},
{
"level_": 0,
@ -25,7 +25,7 @@
"fieldsDescending": {}
},
"memoizedSize": -1,
"memoizedHashCode": 431825513
"memoizedHashCode": 1697297893
},
{
"level_": 0,
@ -39,6 +39,6 @@
"fieldsDescending": {}
},
"memoizedSize": -1,
"memoizedHashCode": -779738141
"memoizedHashCode": 485734239
}
]

@ -178,3 +178,15 @@ create_cxx_tasks
create-initial-cxx-model completed in 89ms
create_cxx_tasks completed in 90ms
# C/C++ build system timings
create_cxx_tasks
create-initial-cxx-model
create-module-model
create-cmake-model 36ms
create-module-model completed in 38ms
create-module-model
create-cmake-model 38ms
create-module-model completed in 40ms
create-initial-cxx-model completed in 94ms
create_cxx_tasks completed in 94ms

@ -1151,3 +1151,15 @@ create_cxx_tasks
create-initial-cxx-model completed in 85ms
create_cxx_tasks completed in 87ms
# C/C++ build system timings
create_cxx_tasks
create-initial-cxx-model
create-module-model
create-cmake-model 39ms
create-module-model completed in 41ms
create-module-model
create-cmake-model 36ms
create-module-model completed in 38ms
create-initial-cxx-model completed in 97ms
create_cxx_tasks completed in 99ms

@ -11,7 +11,7 @@
"fieldsDescending": {}
},
"memoizedSize": -1,
"memoizedHashCode": 1692822936
"memoizedHashCode": -1660375688
},
{
"level_": 0,
@ -25,7 +25,7 @@
"fieldsDescending": {}
},
"memoizedSize": -1,
"memoizedHashCode": -1809956257
"memoizedHashCode": -868187585
},
{
"level_": 0,
@ -39,7 +39,7 @@
"fieldsDescending": {}
},
"memoizedSize": -1,
"memoizedHashCode": -1105495064
"memoizedHashCode": -163726392
},
{
"level_": 0,
@ -53,7 +53,7 @@
"fieldsDescending": {}
},
"memoizedSize": -1,
"memoizedHashCode": 757319006
"memoizedHashCode": 1699087678
},
{
"level_": 0,
@ -67,7 +67,7 @@
"fieldsDescending": {}
},
"memoizedSize": -1,
"memoizedHashCode": -135370576
"memoizedHashCode": 806398096
},
{
"level_": 0,
@ -81,6 +81,6 @@
"fieldsDescending": {}
},
"memoizedSize": -1,
"memoizedHashCode": 200609643
"memoizedHashCode": 1142378315
}
]
Loading…
Cancel
Save