diff --git a/app/aisvc/rpc/aisvc.proto b/app/aisvc/rpc/aisvc.proto index 4fe4e2c..c181665 100644 --- a/app/aisvc/rpc/aisvc.proto +++ b/app/aisvc/rpc/aisvc.proto @@ -60,6 +60,17 @@ message ModifyFaceTypeRequest { message ModifyFaceTypeResponse { string result = 1; } + +// 模糊图片搜索 +message ImageClarityRequest { + bytes image = 1; +} +message ImageClarityResponse { + bool is_blurred = 1; + float confidence = 2; +} + + service AiService { // FaceRecognition rpc FaceRecognition (FaceRecognitionRequest) returns (FaceRecognitionResponse); @@ -73,6 +84,8 @@ service AiService { rpc ModifyFaceName (ModifyFaceNameRequest) returns (ModifyFaceNameResponse); // ModifyFaceType rpc ModifyFaceType (ModifyFaceTypeRequest) returns (ModifyFaceTypeResponse); + // FuzzySearch + rpc ImageClarity (ImageClarityRequest) returns (ImageClarityResponse); } diff --git a/app/aisvc/rpc/client/aiservice/ai_service.go b/app/aisvc/rpc/client/aiservice/ai_service.go index 0e8bb03..cab2972 100644 --- a/app/aisvc/rpc/client/aiservice/ai_service.go +++ b/app/aisvc/rpc/client/aiservice/ai_service.go @@ -19,6 +19,8 @@ type ( FaceLibrary = pb.FaceLibrary FaceRecognitionRequest = pb.FaceRecognitionRequest FaceRecognitionResponse = pb.FaceRecognitionResponse + ImageClarityRequest = pb.ImageClarityRequest + ImageClarityResponse = pb.ImageClarityResponse ModifyFaceNameRequest = pb.ModifyFaceNameRequest ModifyFaceNameResponse = pb.ModifyFaceNameResponse ModifyFaceTypeRequest = pb.ModifyFaceTypeRequest @@ -41,6 +43,8 @@ type ( ModifyFaceName(ctx context.Context, in *ModifyFaceNameRequest, opts ...grpc.CallOption) (*ModifyFaceNameResponse, error) // ModifyFaceType ModifyFaceType(ctx context.Context, in *ModifyFaceTypeRequest, opts ...grpc.CallOption) (*ModifyFaceTypeResponse, error) + // FuzzySearch + ImageClarity(ctx context.Context, in *ImageClarityRequest, opts ...grpc.CallOption) (*ImageClarityResponse, error) } defaultAiService struct { @@ -89,3 +93,9 @@ func (m *defaultAiService) ModifyFaceType(ctx context.Context, in *ModifyFaceTyp client := pb.NewAiServiceClient(m.cli.Conn()) return client.ModifyFaceType(ctx, in, opts...) } + +// FuzzySearch +func (m *defaultAiService) ImageClarity(ctx context.Context, in *ImageClarityRequest, opts ...grpc.CallOption) (*ImageClarityResponse, error) { + client := pb.NewAiServiceClient(m.cli.Conn()) + return client.ImageClarity(ctx, in, opts...) +} diff --git a/app/aisvc/rpc/internal/logic/aiservice/image_clarity_logic.go b/app/aisvc/rpc/internal/logic/aiservice/image_clarity_logic.go new file mode 100644 index 0000000..1ee75cf --- /dev/null +++ b/app/aisvc/rpc/internal/logic/aiservice/image_clarity_logic.go @@ -0,0 +1,36 @@ +package aiservicelogic + +import ( + "context" + + "schisandra-album-cloud-microservices/app/aisvc/rpc/internal/svc" + "schisandra-album-cloud-microservices/app/aisvc/rpc/pb" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ImageClarityLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewImageClarityLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ImageClarityLogic { + return &ImageClarityLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// ImageClarity 图像清晰度检测 +func (l *ImageClarityLogic) ImageClarity(in *pb.ImageClarityRequest) (*pb.ImageClarityResponse, error) { + blurred, confidence, err := l.svcCtx.Clarity.Detect(in.Image) + if err != nil { + return nil, err + } + return &pb.ImageClarityResponse{ + IsBlurred: blurred, + Confidence: float32(confidence), + }, nil +} diff --git a/app/aisvc/rpc/internal/server/aiservice/ai_service_server.go b/app/aisvc/rpc/internal/server/aiservice/ai_service_server.go index 3838cf0..af4bcc4 100644 --- a/app/aisvc/rpc/internal/server/aiservice/ai_service_server.go +++ b/app/aisvc/rpc/internal/server/aiservice/ai_service_server.go @@ -58,3 +58,9 @@ func (s *AiServiceServer) ModifyFaceType(ctx context.Context, in *pb.ModifyFaceT l := aiservicelogic.NewModifyFaceTypeLogic(ctx, s.svcCtx) return l.ModifyFaceType(in) } + +// FuzzySearch +func (s *AiServiceServer) ImageClarity(ctx context.Context, in *pb.ImageClarityRequest) (*pb.ImageClarityResponse, error) { + l := aiservicelogic.NewImageClarityLogic(ctx, s.svcCtx) + return l.ImageClarity(in) +} diff --git a/app/aisvc/rpc/internal/svc/service_context.go b/app/aisvc/rpc/internal/svc/service_context.go index a9ecd8d..af2ed31 100644 --- a/app/aisvc/rpc/internal/svc/service_context.go +++ b/app/aisvc/rpc/internal/svc/service_context.go @@ -9,6 +9,7 @@ import ( "schisandra-album-cloud-microservices/app/aisvc/model/mysql/query" "schisandra-album-cloud-microservices/app/aisvc/rpc/internal/config" "schisandra-album-cloud-microservices/common/caffe_classifier" + "schisandra-album-cloud-microservices/common/clarity" "schisandra-album-cloud-microservices/common/face_recognizer" "schisandra-album-cloud-microservices/common/miniox" "schisandra-album-cloud-microservices/common/redisx" @@ -25,6 +26,7 @@ type ServiceContext struct { CaffeNet *gocv.Net CaffeDesc []string MinioClient *minio.Client + Clarity *clarity.Detector } func NewServiceContext(c config.Config) *ServiceContext { @@ -42,5 +44,6 @@ func NewServiceContext(c config.Config) *ServiceContext { CaffeNet: caffeClassifier, CaffeDesc: caffeDesc, MinioClient: miniox.NewMinio(c.Minio.Endpoint, c.Minio.AccessKeyID, c.Minio.SecretAccessKey, c.Minio.UseSSL), + Clarity: clarity.NewDetector(clarity.WithConcurrency(8), clarity.WithBaseThreshold(90), clarity.WithEdgeBoost(1.2), clarity.WithSampleScale(1)), } } diff --git a/app/aisvc/rpc/pb/aisvc.pb.go b/app/aisvc/rpc/pb/aisvc.pb.go index b7ef05a..7f3f8af 100644 --- a/app/aisvc/rpc/pb/aisvc.pb.go +++ b/app/aisvc/rpc/pb/aisvc.pb.go @@ -699,6 +699,105 @@ func (x *ModifyFaceTypeResponse) GetResult() string { return "" } +// 模糊图片搜索 +type ImageClarityRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Image []byte `protobuf:"bytes,1,opt,name=image,proto3" json:"image,omitempty"` +} + +func (x *ImageClarityRequest) Reset() { + *x = ImageClarityRequest{} + mi := &file_aisvc_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ImageClarityRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ImageClarityRequest) ProtoMessage() {} + +func (x *ImageClarityRequest) ProtoReflect() protoreflect.Message { + mi := &file_aisvc_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ImageClarityRequest.ProtoReflect.Descriptor instead. +func (*ImageClarityRequest) Descriptor() ([]byte, []int) { + return file_aisvc_proto_rawDescGZIP(), []int{13} +} + +func (x *ImageClarityRequest) GetImage() []byte { + if x != nil { + return x.Image + } + return nil +} + +type ImageClarityResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + IsBlurred bool `protobuf:"varint,1,opt,name=is_blurred,json=isBlurred,proto3" json:"is_blurred,omitempty"` + Confidence float32 `protobuf:"fixed32,2,opt,name=confidence,proto3" json:"confidence,omitempty"` +} + +func (x *ImageClarityResponse) Reset() { + *x = ImageClarityResponse{} + mi := &file_aisvc_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ImageClarityResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ImageClarityResponse) ProtoMessage() {} + +func (x *ImageClarityResponse) ProtoReflect() protoreflect.Message { + mi := &file_aisvc_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ImageClarityResponse.ProtoReflect.Descriptor instead. +func (*ImageClarityResponse) Descriptor() ([]byte, []int) { + return file_aisvc_proto_rawDescGZIP(), []int{14} +} + +func (x *ImageClarityResponse) GetIsBlurred() bool { + if x != nil { + return x.IsBlurred + } + return false +} + +func (x *ImageClarityResponse) GetConfidence() float32 { + if x != nil { + return x.Confidence + } + return 0 +} + var File_aisvc_proto protoreflect.FileDescriptor var file_aisvc_proto_rawDesc = []byte{ @@ -762,38 +861,50 @@ var file_aisvc_proto_rawDesc = []byte{ 0x79, 0x70, 0x65, 0x22, 0x30, 0x0a, 0x16, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x79, 0x46, 0x61, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, - 0x65, 0x73, 0x75, 0x6c, 0x74, 0x32, 0xdf, 0x03, 0x0a, 0x09, 0x41, 0x69, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x12, 0x4a, 0x0a, 0x0f, 0x46, 0x61, 0x63, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x67, - 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x2e, 0x61, 0x69, 0x2e, 0x46, 0x61, 0x63, 0x65, - 0x52, 0x65, 0x63, 0x6f, 0x67, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x69, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x52, 0x65, 0x63, 0x6f, - 0x67, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x4d, 0x0a, 0x10, 0x54, 0x66, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x2e, 0x61, 0x69, 0x2e, 0x54, 0x66, 0x43, 0x6c, 0x61, 0x73, 0x73, - 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1c, 0x2e, 0x61, 0x69, 0x2e, 0x54, 0x66, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x69, 0x66, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, - 0x0a, 0x13, 0x43, 0x61, 0x66, 0x66, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x69, 0x66, 0x69, 0x63, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x2e, 0x61, 0x69, 0x2e, 0x43, 0x61, 0x66, 0x66, 0x65, - 0x43, 0x6c, 0x61, 0x73, 0x73, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x61, 0x69, 0x2e, 0x43, 0x61, 0x66, 0x66, 0x65, - 0x43, 0x6c, 0x61, 0x73, 0x73, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x10, 0x51, 0x75, 0x65, 0x72, 0x79, 0x46, - 0x61, 0x63, 0x65, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x12, 0x1b, 0x2e, 0x61, 0x69, 0x2e, - 0x51, 0x75, 0x65, 0x72, 0x79, 0x46, 0x61, 0x63, 0x65, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x69, 0x2e, 0x51, 0x75, 0x65, - 0x72, 0x79, 0x46, 0x61, 0x63, 0x65, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x0e, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x79, 0x46, - 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x69, 0x2e, 0x4d, 0x6f, 0x64, - 0x69, 0x66, 0x79, 0x46, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x61, 0x69, 0x2e, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x79, 0x46, 0x61, - 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, - 0x0a, 0x0e, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x79, 0x46, 0x61, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, - 0x12, 0x19, 0x2e, 0x61, 0x69, 0x2e, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x79, 0x46, 0x61, 0x63, 0x65, - 0x54, 0x79, 0x70, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x61, 0x69, - 0x2e, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x79, 0x46, 0x61, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x06, 0x5a, 0x04, 0x2e, 0x2f, 0x70, 0x62, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x2b, 0x0a, 0x13, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x43, 0x6c, + 0x61, 0x72, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, + 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x69, 0x6d, 0x61, + 0x67, 0x65, 0x22, 0x55, 0x0a, 0x14, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x43, 0x6c, 0x61, 0x72, 0x69, + 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, + 0x5f, 0x62, 0x6c, 0x75, 0x72, 0x72, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, + 0x69, 0x73, 0x42, 0x6c, 0x75, 0x72, 0x72, 0x65, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x02, 0x52, 0x0a, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x32, 0xa2, 0x04, 0x0a, 0x09, 0x41, 0x69, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4a, 0x0a, 0x0f, 0x46, 0x61, 0x63, 0x65, 0x52, + 0x65, 0x63, 0x6f, 0x67, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x2e, 0x61, 0x69, 0x2e, + 0x46, 0x61, 0x63, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x67, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x69, 0x2e, 0x46, 0x61, 0x63, 0x65, + 0x52, 0x65, 0x63, 0x6f, 0x67, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x10, 0x54, 0x66, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x2e, 0x61, 0x69, 0x2e, 0x54, 0x66, 0x43, + 0x6c, 0x61, 0x73, 0x73, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x69, 0x2e, 0x54, 0x66, 0x43, 0x6c, 0x61, 0x73, + 0x73, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x56, 0x0a, 0x13, 0x43, 0x61, 0x66, 0x66, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x2e, 0x61, 0x69, 0x2e, 0x43, + 0x61, 0x66, 0x66, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x61, 0x69, 0x2e, 0x43, + 0x61, 0x66, 0x66, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x10, 0x51, 0x75, + 0x65, 0x72, 0x79, 0x46, 0x61, 0x63, 0x65, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x12, 0x1b, + 0x2e, 0x61, 0x69, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x46, 0x61, 0x63, 0x65, 0x4c, 0x69, 0x62, + 0x72, 0x61, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x69, + 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x46, 0x61, 0x63, 0x65, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, + 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x0e, 0x4d, 0x6f, 0x64, + 0x69, 0x66, 0x79, 0x46, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x69, + 0x2e, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x79, 0x46, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x61, 0x69, 0x2e, 0x4d, 0x6f, 0x64, 0x69, + 0x66, 0x79, 0x46, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x47, 0x0a, 0x0e, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x79, 0x46, 0x61, 0x63, 0x65, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x69, 0x2e, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x79, + 0x46, 0x61, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1a, 0x2e, 0x61, 0x69, 0x2e, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x79, 0x46, 0x61, 0x63, 0x65, 0x54, + 0x79, 0x70, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x49, + 0x6d, 0x61, 0x67, 0x65, 0x43, 0x6c, 0x61, 0x72, 0x69, 0x74, 0x79, 0x12, 0x17, 0x2e, 0x61, 0x69, + 0x2e, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x43, 0x6c, 0x61, 0x72, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x69, 0x2e, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x43, + 0x6c, 0x61, 0x72, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x06, + 0x5a, 0x04, 0x2e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -808,7 +919,7 @@ func file_aisvc_proto_rawDescGZIP() []byte { return file_aisvc_proto_rawDescData } -var file_aisvc_proto_msgTypes = make([]protoimpl.MessageInfo, 13) +var file_aisvc_proto_msgTypes = make([]protoimpl.MessageInfo, 15) var file_aisvc_proto_goTypes = []any{ (*FaceRecognitionRequest)(nil), // 0: ai.FaceRecognitionRequest (*FaceRecognitionResponse)(nil), // 1: ai.FaceRecognitionResponse @@ -823,6 +934,8 @@ var file_aisvc_proto_goTypes = []any{ (*ModifyFaceNameResponse)(nil), // 10: ai.ModifyFaceNameResponse (*ModifyFaceTypeRequest)(nil), // 11: ai.ModifyFaceTypeRequest (*ModifyFaceTypeResponse)(nil), // 12: ai.ModifyFaceTypeResponse + (*ImageClarityRequest)(nil), // 13: ai.ImageClarityRequest + (*ImageClarityResponse)(nil), // 14: ai.ImageClarityResponse } var file_aisvc_proto_depIdxs = []int32{ 7, // 0: ai.QueryFaceLibraryResponse.faces:type_name -> ai.FaceLibrary @@ -832,14 +945,16 @@ var file_aisvc_proto_depIdxs = []int32{ 6, // 4: ai.AiService.QueryFaceLibrary:input_type -> ai.QueryFaceLibraryRequest 9, // 5: ai.AiService.ModifyFaceName:input_type -> ai.ModifyFaceNameRequest 11, // 6: ai.AiService.ModifyFaceType:input_type -> ai.ModifyFaceTypeRequest - 1, // 7: ai.AiService.FaceRecognition:output_type -> ai.FaceRecognitionResponse - 3, // 8: ai.AiService.TfClassification:output_type -> ai.TfClassificationResponse - 5, // 9: ai.AiService.CaffeClassification:output_type -> ai.CaffeClassificationResponse - 8, // 10: ai.AiService.QueryFaceLibrary:output_type -> ai.QueryFaceLibraryResponse - 10, // 11: ai.AiService.ModifyFaceName:output_type -> ai.ModifyFaceNameResponse - 12, // 12: ai.AiService.ModifyFaceType:output_type -> ai.ModifyFaceTypeResponse - 7, // [7:13] is the sub-list for method output_type - 1, // [1:7] is the sub-list for method input_type + 13, // 7: ai.AiService.ImageClarity:input_type -> ai.ImageClarityRequest + 1, // 8: ai.AiService.FaceRecognition:output_type -> ai.FaceRecognitionResponse + 3, // 9: ai.AiService.TfClassification:output_type -> ai.TfClassificationResponse + 5, // 10: ai.AiService.CaffeClassification:output_type -> ai.CaffeClassificationResponse + 8, // 11: ai.AiService.QueryFaceLibrary:output_type -> ai.QueryFaceLibraryResponse + 10, // 12: ai.AiService.ModifyFaceName:output_type -> ai.ModifyFaceNameResponse + 12, // 13: ai.AiService.ModifyFaceType:output_type -> ai.ModifyFaceTypeResponse + 14, // 14: ai.AiService.ImageClarity:output_type -> ai.ImageClarityResponse + 8, // [8:15] is the sub-list for method output_type + 1, // [1:8] is the sub-list for method input_type 1, // [1:1] is the sub-list for extension type_name 1, // [1:1] is the sub-list for extension extendee 0, // [0:1] is the sub-list for field type_name @@ -856,7 +971,7 @@ func file_aisvc_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_aisvc_proto_rawDesc, NumEnums: 0, - NumMessages: 13, + NumMessages: 15, NumExtensions: 0, NumServices: 1, }, diff --git a/app/aisvc/rpc/pb/aisvc_grpc.pb.go b/app/aisvc/rpc/pb/aisvc_grpc.pb.go index f9cc5a8..e9cd185 100644 --- a/app/aisvc/rpc/pb/aisvc_grpc.pb.go +++ b/app/aisvc/rpc/pb/aisvc_grpc.pb.go @@ -25,6 +25,7 @@ const ( AiService_QueryFaceLibrary_FullMethodName = "/ai.AiService/QueryFaceLibrary" AiService_ModifyFaceName_FullMethodName = "/ai.AiService/ModifyFaceName" AiService_ModifyFaceType_FullMethodName = "/ai.AiService/ModifyFaceType" + AiService_ImageClarity_FullMethodName = "/ai.AiService/ImageClarity" ) // AiServiceClient is the client API for AiService service. @@ -43,6 +44,8 @@ type AiServiceClient interface { ModifyFaceName(ctx context.Context, in *ModifyFaceNameRequest, opts ...grpc.CallOption) (*ModifyFaceNameResponse, error) // ModifyFaceType ModifyFaceType(ctx context.Context, in *ModifyFaceTypeRequest, opts ...grpc.CallOption) (*ModifyFaceTypeResponse, error) + // FuzzySearch + ImageClarity(ctx context.Context, in *ImageClarityRequest, opts ...grpc.CallOption) (*ImageClarityResponse, error) } type aiServiceClient struct { @@ -113,6 +116,16 @@ func (c *aiServiceClient) ModifyFaceType(ctx context.Context, in *ModifyFaceType return out, nil } +func (c *aiServiceClient) ImageClarity(ctx context.Context, in *ImageClarityRequest, opts ...grpc.CallOption) (*ImageClarityResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ImageClarityResponse) + err := c.cc.Invoke(ctx, AiService_ImageClarity_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // AiServiceServer is the server API for AiService service. // All implementations must embed UnimplementedAiServiceServer // for forward compatibility. @@ -129,6 +142,8 @@ type AiServiceServer interface { ModifyFaceName(context.Context, *ModifyFaceNameRequest) (*ModifyFaceNameResponse, error) // ModifyFaceType ModifyFaceType(context.Context, *ModifyFaceTypeRequest) (*ModifyFaceTypeResponse, error) + // FuzzySearch + ImageClarity(context.Context, *ImageClarityRequest) (*ImageClarityResponse, error) mustEmbedUnimplementedAiServiceServer() } @@ -157,6 +172,9 @@ func (UnimplementedAiServiceServer) ModifyFaceName(context.Context, *ModifyFaceN func (UnimplementedAiServiceServer) ModifyFaceType(context.Context, *ModifyFaceTypeRequest) (*ModifyFaceTypeResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ModifyFaceType not implemented") } +func (UnimplementedAiServiceServer) ImageClarity(context.Context, *ImageClarityRequest) (*ImageClarityResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ImageClarity not implemented") +} func (UnimplementedAiServiceServer) mustEmbedUnimplementedAiServiceServer() {} func (UnimplementedAiServiceServer) testEmbeddedByValue() {} @@ -286,6 +304,24 @@ func _AiService_ModifyFaceType_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _AiService_ImageClarity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ImageClarityRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AiServiceServer).ImageClarity(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AiService_ImageClarity_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AiServiceServer).ImageClarity(ctx, req.(*ImageClarityRequest)) + } + return interceptor(ctx, in, info, handler) +} + // AiService_ServiceDesc is the grpc.ServiceDesc for AiService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -317,6 +353,10 @@ var AiService_ServiceDesc = grpc.ServiceDesc{ MethodName: "ModifyFaceType", Handler: _AiService_ModifyFaceType_Handler, }, + { + MethodName: "ImageClarity", + Handler: _AiService_ImageClarity_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "aisvc.proto", diff --git a/app/auth/api/auth.api b/app/auth/api/auth.api index 9a87b20..b6d7f29 100644 --- a/app/auth/api/auth.api +++ b/app/auth/api/auth.api @@ -434,7 +434,7 @@ service auth { // 文件上传配置请求参数 type ( StorageConfigRequest { - Type string `json:"type"` + Provider string `json:"provider"` AccessKey string `json:"access_key"` SecretKey string `json:"secret_key"` Endpoint string `json:"endpoint"` @@ -664,11 +664,10 @@ type ( } // 搜索图片请求参数 SearchImageRequest { - Type string `json:"type"` - Keyword string `json:"keyword"` - Provider string `json:"provider"` - Bucket string `json:"bucket"` - InputImage string `json:"input_image,omitempty"` + Type string `json:"type"` + Keyword string `json:"keyword"` + Provider string `json:"provider"` + Bucket string `json:"bucket"` } // 搜索图片相应参数 SearchImageResponse { @@ -689,6 +688,23 @@ type ( Provider string `json:"provider"` Bucket string `json:"bucket"` } + StorageConfigMeta { + ID int64 `json:"id"` + Provider string `json:"provider"` + Endpoint string `json:"endpoint"` + Bucket string `json:"bucket"` + Region string `json:"region"` + Capacity int64 `json:"capacity"` + CreatedAt string `json:"created_at"` + } + StorageConfigListResponse { + Records []StorageConfigMeta `json:"records"` + } + DeleteStorageConfigRequest { + ID int64 `json:"id"` + Provider string `json:"provider"` + Bucket string `json:"bucket"` + } ) // 文件上传 @@ -710,7 +726,7 @@ service auth { // 设置存储配置 @handler setStorageConfig - post /config (StorageConfigRequest) returns (string) + post /config/add (StorageConfigRequest) returns (string) // 获取人脸样本库列表 @handler getFaceSampleLibraryList @@ -811,6 +827,14 @@ service auth { // 添加图片到相册 @handler addImageToAlbum post /album/add/image (AddImageToAlbumRequest) returns (string) + + //列举用户所有存储商 + @handler listUserStorage + post /user/storage/list returns (StorageConfigListResponse) + + // 删除存储配置 + @handler deleteStorageConfig + post /config/delete (DeleteStorageConfigRequest) returns (string) } type ( diff --git a/app/auth/api/internal/handler/routes.go b/app/auth/api/internal/handler/routes.go index d68f8b4..2f63f1c 100644 --- a/app/auth/api/internal/handler/routes.go +++ b/app/auth/api/internal/handler/routes.go @@ -309,9 +309,14 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { }, { Method: http.MethodPost, - Path: "/config", + Path: "/config/add", Handler: storage.SetStorageConfigHandler(serverCtx), }, + { + Method: http.MethodPost, + Path: "/config/delete", + Handler: storage.DeleteStorageConfigHandler(serverCtx), + }, { Method: http.MethodPost, Path: "/delete/record", @@ -392,6 +397,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/user/config/list", Handler: storage.GetUserStorageListHandler(serverCtx), }, + { + Method: http.MethodPost, + Path: "/user/storage/list", + Handler: storage.ListUserStorageHandler(serverCtx), + }, }..., ), rest.WithJwt(serverCtx.Config.Auth.AccessSecret), diff --git a/app/auth/api/internal/handler/storage/delete_storage_config_handler.go b/app/auth/api/internal/handler/storage/delete_storage_config_handler.go new file mode 100644 index 0000000..7643a41 --- /dev/null +++ b/app/auth/api/internal/handler/storage/delete_storage_config_handler.go @@ -0,0 +1,29 @@ +package storage + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "schisandra-album-cloud-microservices/app/auth/api/internal/logic/storage" + "schisandra-album-cloud-microservices/app/auth/api/internal/svc" + "schisandra-album-cloud-microservices/app/auth/api/internal/types" + "schisandra-album-cloud-microservices/common/xhttp" +) + +func DeleteStorageConfigHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.DeleteStorageConfigRequest + if err := httpx.Parse(r, &req); err != nil { + xhttp.JsonBaseResponseCtx(r.Context(), w, err) + return + } + + l := storage.NewDeleteStorageConfigLogic(r.Context(), svcCtx) + resp, err := l.DeleteStorageConfig(&req) + if err != nil { + xhttp.JsonBaseResponseCtx(r.Context(), w, err) + } else { + xhttp.JsonBaseResponseCtx(r.Context(), w, resp) + } + } +} diff --git a/app/auth/api/internal/handler/storage/list_user_storage_handler.go b/app/auth/api/internal/handler/storage/list_user_storage_handler.go new file mode 100644 index 0000000..1ee90f7 --- /dev/null +++ b/app/auth/api/internal/handler/storage/list_user_storage_handler.go @@ -0,0 +1,21 @@ +package storage + +import ( + "net/http" + + "schisandra-album-cloud-microservices/app/auth/api/internal/logic/storage" + "schisandra-album-cloud-microservices/app/auth/api/internal/svc" + "schisandra-album-cloud-microservices/common/xhttp" +) + +func ListUserStorageHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := storage.NewListUserStorageLogic(r.Context(), svcCtx) + resp, err := l.ListUserStorage() + if err != nil { + xhttp.JsonBaseResponseCtx(r.Context(), w, err) + } else { + xhttp.JsonBaseResponseCtx(r.Context(), w, resp) + } + } +} diff --git a/app/auth/api/internal/logic/storage/delete_storage_config_logic.go b/app/auth/api/internal/logic/storage/delete_storage_config_logic.go new file mode 100644 index 0000000..b6eace5 --- /dev/null +++ b/app/auth/api/internal/logic/storage/delete_storage_config_logic.go @@ -0,0 +1,48 @@ +package storage + +import ( + "context" + "errors" + "schisandra-album-cloud-microservices/common/constant" + + "schisandra-album-cloud-microservices/app/auth/api/internal/svc" + "schisandra-album-cloud-microservices/app/auth/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeleteStorageConfigLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewDeleteStorageConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteStorageConfigLogic { + return &DeleteStorageConfigLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteStorageConfigLogic) DeleteStorageConfig(req *types.DeleteStorageConfigRequest) (resp string, err error) { + uid, ok := l.ctx.Value("user_id").(string) + if !ok { + return "", errors.New("user_id not found") + } + storageConfig := l.svcCtx.DB.ScaStorageConfig + info, err := storageConfig.Where(storageConfig.ID.Eq(req.ID), storageConfig.UserID.Eq(uid), + storageConfig.Provider.Eq(req.Provider), storageConfig.Bucket.Eq(req.Bucket)).Delete() + if err != nil { + return "", err + } + if info.RowsAffected == 0 { + return "", errors.New("storage config not found") + } + cacheOssConfigKey := constant.UserOssConfigPrefix + uid + ":" + req.Provider + err = l.svcCtx.RedisClient.Del(l.ctx, cacheOssConfigKey).Err() + if err != nil { + return "", err + } + return "success", nil +} diff --git a/app/auth/api/internal/logic/storage/list_user_storage_logic.go b/app/auth/api/internal/logic/storage/list_user_storage_logic.go new file mode 100644 index 0000000..e4b8fca --- /dev/null +++ b/app/auth/api/internal/logic/storage/list_user_storage_logic.go @@ -0,0 +1,63 @@ +package storage + +import ( + "context" + "errors" + + "schisandra-album-cloud-microservices/app/auth/api/internal/svc" + "schisandra-album-cloud-microservices/app/auth/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListUserStorageLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewListUserStorageLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListUserStorageLogic { + return &ListUserStorageLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ListUserStorageLogic) ListUserStorage() (resp *types.StorageConfigListResponse, err error) { + uid, ok := l.ctx.Value("user_id").(string) + if !ok { + return nil, errors.New("user_id not found") + } + storageConfig := l.svcCtx.DB.ScaStorageConfig + + storageConfigList, err := storageConfig.Select( + storageConfig.ID, + storageConfig.Provider, + storageConfig.Bucket, + storageConfig.Capacity, + storageConfig.CreatedAt, + storageConfig.Region, + storageConfig.Endpoint). + Where(storageConfig.UserID.Eq(uid)). + Order(storageConfig.CreatedAt.Desc()). + Find() + if err != nil { + return nil, err + } + var result []types.StorageConfigMeta + for _, info := range storageConfigList { + result = append(result, types.StorageConfigMeta{ + ID: info.ID, + Provider: info.Provider, + Endpoint: info.Endpoint, + Bucket: info.Bucket, + Capacity: info.Capacity, + Region: info.Region, + CreatedAt: info.CreatedAt.Format("2006-01-02"), + }) + } + return &types.StorageConfigListResponse{ + Records: result, + }, nil +} diff --git a/app/auth/api/internal/logic/storage/search_image_logic.go b/app/auth/api/internal/logic/storage/search_image_logic.go index 378f65e..6b2b249 100644 --- a/app/auth/api/internal/logic/storage/search_image_logic.go +++ b/app/auth/api/internal/logic/storage/search_image_logic.go @@ -70,7 +70,7 @@ func (l *SearchImageLogic) SearchImage(req *types.SearchImageRequest) (resp *typ // 标签和分类匹配 addThingQuery(baseQuery, req.Keyword) case "picture": - // 图片属性匹配(示例:文件类型) + // 图片属性匹配 addPictureQuery(baseQuery, req.Keyword) case "location": addLocationQuery(baseQuery, req.Keyword) @@ -213,8 +213,8 @@ func addTimeRangeQuery(query map[string]interface{}, start, end int64) { timeQuery := map[string]interface{}{ "range": map[string]interface{}{ "created_at": map[string]interface{}{ // 改为使用 created_at 字段 - "gte": start * 1000, // 转换为毫秒(根据格式决定) - "lte": end * 1000, + "gte": start, + "lte": end, }, }, } @@ -243,12 +243,12 @@ func addPictureQuery(query map[string]interface{}, keyword string) { "should": []map[string]interface{}{ { "wildcard": map[string]interface{}{ - "file_name": "*" + strings.ToLower(keyword) + "*", + "tag_name": "*" + strings.ToLower(keyword) + "*", }, }, { "term": map[string]interface{}{ - "file_type": strings.ToLower(keyword), + "top_category": strings.ToLower(keyword), }, }, }, @@ -264,13 +264,13 @@ func addPictureQuery(query map[string]interface{}, keyword string) { // 添加人脸ID查询 func addFaceIDQuery(query map[string]interface{}, faceID int64) { - must := query["query"].(map[string]interface{})["bool"].(map[string]interface{})["must"] + must := query["query"].(map[string]interface{})["bool"].(map[string]interface{})["must"].([]map[string]interface{}) idQuery := map[string]interface{}{ "term": map[string]interface{}{ "face_id": faceID, }, } - query["query"].(map[string]interface{})["bool"].(map[string]interface{})["must"] = append(must.([]map[string]interface{}), idQuery) + query["query"].(map[string]interface{})["bool"].(map[string]interface{})["must"] = append(must, idQuery) } func addLocationQuery(query map[string]interface{}, keyword string) { diff --git a/app/auth/api/internal/logic/storage/set_storage_config_logic.go b/app/auth/api/internal/logic/storage/set_storage_config_logic.go index 463f536..eec030d 100644 --- a/app/auth/api/internal/logic/storage/set_storage_config_logic.go +++ b/app/auth/api/internal/logic/storage/set_storage_config_logic.go @@ -40,7 +40,7 @@ func (l *SetStorageConfigLogic) SetStorageConfig(req *types.StorageConfigRequest } ossConfig := &model.ScaStorageConfig{ UserID: uid, - Provider: req.Type, + Provider: req.Provider, Endpoint: req.Endpoint, Bucket: req.Bucket, AccessKey: accessKey, diff --git a/app/auth/api/internal/types/types.go b/app/auth/api/internal/types/types.go index 9abe775..05d3e44 100644 --- a/app/auth/api/internal/types/types.go +++ b/app/auth/api/internal/types/types.go @@ -182,6 +182,12 @@ type DeleteShareRecordRequest struct { AlbumID int64 `json:"album_id"` } +type DeleteStorageConfigRequest struct { + ID int64 `json:"id"` + Provider string `json:"provider"` + Bucket string `json:"bucket"` +} + type DownloadAlbumRequest struct { ID int64 `json:"id"` Provider string `json:"provider"` @@ -384,11 +390,10 @@ type SearchAlbumResponse struct { } type SearchImageRequest struct { - Type string `json:"type"` - Keyword string `json:"keyword"` - Provider string `json:"provider"` - Bucket string `json:"bucket"` - InputImage string `json:"input_image,omitempty"` + Type string `json:"type"` + Keyword string `json:"keyword"` + Provider string `json:"provider"` + Bucket string `json:"bucket"` } type SearchImageResponse struct { @@ -495,8 +500,22 @@ type SmsSendRequest struct { Key string `json:"key"` } +type StorageConfigListResponse struct { + Records []StorageConfigMeta `json:"records"` +} + +type StorageConfigMeta struct { + ID int64 `json:"id"` + Provider string `json:"provider"` + Endpoint string `json:"endpoint"` + Bucket string `json:"bucket"` + Region string `json:"region"` + Capacity int64 `json:"capacity"` + CreatedAt string `json:"created_at"` +} + type StorageConfigRequest struct { - Type string `json:"type"` + Provider string `json:"provider"` AccessKey string `json:"access_key"` SecretKey string `json:"secret_key"` Endpoint string `json:"endpoint"` diff --git a/common/clarity/2.png b/common/clarity/2.png new file mode 100644 index 0000000..2e07d01 Binary files /dev/null and b/common/clarity/2.png differ diff --git a/common/clarity/3.jpg b/common/clarity/3.jpg new file mode 100644 index 0000000..42d9d89 Binary files /dev/null and b/common/clarity/3.jpg differ diff --git a/common/clarity/4.png b/common/clarity/4.png new file mode 100644 index 0000000..f825b48 Binary files /dev/null and b/common/clarity/4.png differ diff --git a/common/clarity/clarity.go b/common/clarity/clarity.go new file mode 100644 index 0000000..dc63766 --- /dev/null +++ b/common/clarity/clarity.go @@ -0,0 +1,260 @@ +package clarity + +import ( + "bytes" + "context" + "image" + _ "image/jpeg" + _ "image/png" + "math" + "runtime" + "sync" + + "golang.org/x/sync/errgroup" + "golang.org/x/sync/semaphore" +) + +// Detector 图片模糊检测器 +type Detector struct { + baseThreshold float64 // 基准阈值 + sampleScale int // 采样比例基数 + edgeBoost float64 // 边缘增强系数 + noiseFloor float64 // 噪声基底 + channelWeights [3]float64 // RGB通道权重 + adaptiveSampling bool // 启用自适应采样 + regionWeights []float64 // 区域权重矩阵 + concurrencyLimit int64 // 最大并发数 + weightedSemaphore *semaphore.Weighted + pool sync.Pool // 内存池 +} + +type Option func(*Detector) + +// NewDetector 创建检测器实例 +func NewDetector(opts ...Option) *Detector { + d := &Detector{ + baseThreshold: 85.0, + sampleScale: 2, + edgeBoost: 1.0, + noiseFloor: 5.0, + channelWeights: [3]float64{0.299, 0.587, 0.114}, + adaptiveSampling: true, + concurrencyLimit: int64(runtime.NumCPU() * 2), + } + + d.pool.New = func() interface{} { + return &scanContext{ + sum: 0, + sumSq: 0, + } + } + + d.weightedSemaphore = semaphore.NewWeighted(d.concurrencyLimit) + + for _, opt := range opts { + opt(d) + } + return d +} + +// 配置选项 --------------------------------------------------- + +func WithBaseThreshold(t float64) Option { + return func(d *Detector) { + d.baseThreshold = t + } +} + +func WithSampleScale(n int) Option { + return func(d *Detector) { + d.sampleScale = 1 << uint(maxInt(0, n)) + } +} + +func WithEdgeBoost(factor float64) Option { + return func(d *Detector) { + d.edgeBoost = clamp(factor, 0.5, 2.0) + } +} + +func WithNoiseFloor(floor float64) Option { + return func(d *Detector) { + d.noiseFloor = math.Max(0, floor) + } +} + +func WithConcurrency(n int) Option { + return func(d *Detector) { + d.concurrencyLimit = int64(maxInt(1, n)) + d.weightedSemaphore = semaphore.NewWeighted(d.concurrencyLimit) + } +} + +// Detect 执行模糊检测 +func (d *Detector) Detect(imgData []byte) (isBlurred bool, confidence float64, err error) { + img, _, err := image.Decode(bytes.NewReader(imgData)) + if err != nil { + return true, 0.0, err + } + + bounds := img.Bounds() + width, height := bounds.Dx(), bounds.Dy() + + if width < 32 || height < 32 { + return true, 0.0, nil + } + + ctx := d.pool.Get().(*scanContext) + defer d.pool.Put(ctx) + ctx.reset() + + step := d.calculateStep(width, height) + + g, groupCtx := errgroup.WithContext(context.Background()) + processingCtx, cancel := context.WithCancel(groupCtx) + defer cancel() + + for y := bounds.Min.Y; y < bounds.Max.Y; y += step { + for x := bounds.Min.X; x < bounds.Max.X; x += step { + x, y := x, y // 捕获循环变量 + + if err := d.weightedSemaphore.Acquire(processingCtx, 1); err != nil { + break + } + + g.Go(func() error { + defer d.weightedSemaphore.Release(1) + + select { + case <-processingCtx.Done(): + return nil + default: + } + + if x <= 0 || y <= 0 || x >= bounds.Max.X-1 || y >= bounds.Max.Y-1 { + return nil + } + + gray := d.calculateGray(img, x, y) + val := d.calculateLaplacian(img, x, y, gray) + weight := d.getRegionWeight(x, y, bounds) + + ctx.mu.Lock() + ctx.sum += val * weight + ctx.sumSq += (val * weight) * (val * weight) + ctx.mu.Unlock() + + return nil + }) + } + } + + if err := g.Wait(); err != nil { + return true, 0.0, err + } + + n := float64(((width / step) * (height / step)) - 4) + if n <= 0 { + return true, 0.0, nil + } + + mean := ctx.sum / n + variance := (ctx.sumSq/n - mean*mean) * 1e6 + + dynamicThreshold := d.calculateDynamicThreshold(width, height) + confidence = math.Max(0, math.Min(1, (variance-d.noiseFloor)/(dynamicThreshold-d.noiseFloor))) + + return variance < dynamicThreshold, confidence, nil +} + +// 私有方法 --------------------------------------------------- + +func (d *Detector) calculateStep(width, height int) int { + if !d.adaptiveSampling { + return d.sampleScale + } + + area := width * height + switch { + case area > 4000*3000: + return d.sampleScale * 4 + case area > 2000*1500: + return d.sampleScale * 2 + default: + return d.sampleScale + } +} + +func (d *Detector) calculateGray(img image.Image, x, y int) float64 { + r, g, b, _ := img.At(x, y).RGBA() + return d.channelWeights[0]*float64(r>>8) + + d.channelWeights[1]*float64(g>>8) + + d.channelWeights[2]*float64(b>>8) +} + +func (d *Detector) calculateLaplacian(img image.Image, x, y int, center float64) float64 { + getGray := func(x, y int) float64 { + r, g, b, _ := img.At(x, y).RGBA() + return d.channelWeights[0]*float64(r>>8) + + d.channelWeights[1]*float64(g>>8) + + d.channelWeights[2]*float64(b>>8) + } + + return math.Abs(4*center- + getGray(x-1, y)- + getGray(x+1, y)- + getGray(x, y-1)- + getGray(x, y+1)) * d.edgeBoost +} + +func (d *Detector) calculateDynamicThreshold(width, height int) float64 { + areaRatio := float64(width*height) / 250000.0 + return d.baseThreshold*math.Pow(areaRatio, 0.65) + d.noiseFloor +} + +func (d *Detector) getRegionWeight(x, y int, bounds image.Rectangle) float64 { + if len(d.regionWeights) == 0 { + return 1.0 + } + + size := int(math.Sqrt(float64(len(d.regionWeights)))) + if size == 0 { + return 1.0 + } + + nx := float64(x-bounds.Min.X) / float64(bounds.Dx()) + ny := float64(y-bounds.Min.Y) / float64(bounds.Dy()) + + ix := int(nx * float64(size)) + iy := int(ny * float64(size)) + idx := iy*size + ix + + if idx >= 0 && idx < len(d.regionWeights) { + return d.regionWeights[idx] + } + return 1.0 +} + +// 辅助函数 --------------------------------------------------- + +type scanContext struct { + sum float64 + sumSq float64 + mu sync.Mutex +} + +func (c *scanContext) reset() { + c.sum = 0 + c.sumSq = 0 +} + +func maxInt(a, b int) int { + if a > b { + return a + } + return b +} + +func clamp(value, min, max float64) float64 { + return math.Max(min, math.Min(max, value)) +} diff --git a/common/clarity/clarity_opencv.go b/common/clarity/clarity_opencv.go new file mode 100644 index 0000000..d8a9f3e --- /dev/null +++ b/common/clarity/clarity_opencv.go @@ -0,0 +1,62 @@ +package clarity + +import ( + "gocv.io/x/gocv" + "image" +) + +// 清晰度检测 +func Clarity(img image.Image) (bool, error) { + mat, err := gocv.ImageToMatRGB(img) + if err != nil || mat.Empty() { + if mat.Empty() == false { + mat.Close() + } + return false, err + } + matClone := mat.Clone() + // 如果图片是多通道 就进去转换 + if mat.Channels() != 1 { + // 将图像转换为灰度显示 + gocv.CvtColor(mat, &matClone, gocv.ColorRGBToGray) + } + mat.Close() + + destCanny := gocv.NewMat() + + destCannyC := gocv.NewMat() + + destCannyD := gocv.NewMat() + // 边缘检测 + gocv.Canny(matClone, &destCanny, 200, 200) + // 求矩阵的均值与标准差 + gocv.MeanStdDev(destCanny, &destCannyC, &destCannyD) + destCanny.Close() + destCannyC.Close() + if destCannyD.GetDoubleAt(0, 0) == 0 { + destCannyD.Close() + matClone.Close() + return false, nil + } + destCannyD.Close() + + destC := gocv.NewMat() + destD := gocv.NewMat() + destA := gocv.NewMat() + // Laplace算子 + gocv.Laplacian(matClone, &destA, gocv.MatTypeCV64F, 3, 1, 0, gocv.BorderDefault) + gocv.MeanStdDev(destA, &destC, &destD) + destC.Close() + destA.Close() + destMean := gocv.NewMat() + gocv.Laplacian(matClone, &destMean, gocv.MatTypeCV16U, 3, 1, 0, gocv.BorderDefault) + mean := destMean.Mean() + destMean.Close() + matClone.Close() + if mean.Val1 > 5 || destD.GetDoubleAt(0, 0) > 20 { + destD.Close() + return true, nil + } + destD.Close() + return false, nil +} diff --git a/common/clarity/clarity_test.go b/common/clarity/clarity_test.go new file mode 100644 index 0000000..2181629 --- /dev/null +++ b/common/clarity/clarity_test.go @@ -0,0 +1,27 @@ +package clarity + +import ( + "bytes" + "image" + "os" + "testing" +) + +func TestClarity(t *testing.T) { + + //detector := NewDetector( + // WithConcurrency(8), WithBaseThreshold(90), WithEdgeBoost(1.2), WithSampleScale(1)) + //imgData, _ := os.ReadFile("4.png") + //blurred, confidence, err := detector.Detect(imgData) + //if err != nil { + // t.Error(err) + //} + //t.Log(blurred, confidence) + imgData, _ := os.ReadFile("2.png") + img, _, err := image.Decode(bytes.NewReader(imgData)) + clarity, err := Clarity(img) + if err != nil { + t.Error(err) + } + t.Log(clarity) +}