λ©λ©ν¬ νλ«νΌμ λΉλ¬Έ(Nose Print) νΉμ§ λ²‘ν° μΆμΆ λ° μ μ¬λ κ²μ¦ μλ²μ λλ€.
ML Serverλ λ°λ €λλ¬Όμ μ½ λ¬΄λ¬(λΉλ¬Έ)λ₯Ό λΆμνμ¬ κ³ μ ν νΉμ§ 벑ν°λ₯Ό μΆμΆνκ³ , μ μ₯λ 벑ν°μμ μ μ¬λλ₯Ό κ²μ¦νλ λ§μ΄ν¬λ‘μλΉμ€μ λλ€.
- νΉμ§ λ²‘ν° μΆμΆ: μ½ μ΄λ―Έμ§μμ κ³ μ ν μλ² λ© λ²‘ν° μμ±
- μ μ¬λ κ²μ¦: μ μ΄λ―Έμ§μ μ μ₯λ λ²‘ν° λΉκ΅ (μ½μ¬μΈ μ μ¬λ, μ ν΄λ¦¬λ 거리)
- Pet DID μμ± μ§μ: μΆμΆλ 벑ν°μ ν΄μλ‘ κ³ μ ν DID μμ±
- Framework: FastAPI (Python)
- Language: Python 3.11+
- ML Runtime: ONNX Runtime
- Model Format: ONNX (Open Neural Network Exchange)
- Communication: gRPC (Protocol Buffers)
- Storage: NCP Object Storage
- Container: Docker
- Orchestration: Kubernetes
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β API Gateway β
β (NestJS) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
β gRPC (:50052)
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ML Server β
β (FastAPI) β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β ONNX Runtime β β
β β β β
β β βββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β embedder_model.onnx β β β
β β β β β β
β β β Input: μ½ μ΄λ―Έμ§ (224x224 RGB) β β β
β β β Output: νΉμ§ λ²‘ν° (512-dim float array) β β β
β β βββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββ
β NCP Object Storageβ
β β
β nose-print-photo/ β
β βββ {petDID}/ β
β βββ img.jpgβ
β βββ vec.npyβ
βββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββ
β PostgreSQL β
β (λ²‘ν° λ©νλ°μ΄ν°)β
βββββββββββββββββββββ
service NoseEmbedderService {
// κ°μμ§ μ½ μ΄λ―Έμ§μμ νΉμ§ λ²‘ν° μΆμΆ
rpc ExtractNoseVector(NoseImageRequest) returns (NoseVectorResponse);
// μ μ΄λ―Έμ§μ μ μ₯λ μ΄λ―Έμ§(PetDID) λΉκ΅
rpc CompareWithStoredImage(CompareWithStoredImageRequest) returns (CompareVectorsResponse);
// gRPC μ°κ²° μν νμΈ
rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);
}μ½ μ΄λ―Έμ§μμ κ³ μ ν νΉμ§ 벑ν°(μλ² λ©)λ₯Ό μΆμΆν©λλ€.
Request:
message NoseImageRequest {
bytes image_data = 1; // μ΄λ―Έμ§ λ°μ΄νΈ λ°μ΄ν° (JPEG/PNG)
string image_format = 2; // μ΄λ―Έμ§ ν¬λ§· ("jpeg" λλ "png")
}Response:
message NoseVectorResponse {
repeated float vector = 1; // νΉμ§ λ²‘ν° (512μ°¨μ float λ°°μ΄)
int32 vector_size = 2; // λ²‘ν° μ°¨μ (512)
bool success = 3; // μ±κ³΅ μ¬λΆ
string error_message = 4; // μλ¬ λ©μμ§
optional MLErrorCode error_code = 5; // μλ¬ μ½λ
optional bool retryable = 6; // μ¬μλ κ°λ₯ μ¬λΆ
}μ¬μ© μμ:
# API Gatewayμμ νΈμΆ
response = await ml_service.ExtractNoseVector(
NoseImageRequest(
image_data=image_bytes,
image_format="jpeg"
)
)
# 벑ν°λ₯Ό keccak256 ν΄μνμ¬ Pet DID μμ±
vector_hash = keccak256(response.vector)
pet_did = f"did:ethr:besu:0x{vector_hash[:40]}"μλ‘ μ΄¬μν μ΄λ―Έμ§μ κΈ°μ‘΄μ λ±λ‘λ ν«μ λΉλ¬Έμ λΉκ΅νμ¬ μ μ¬λλ₯Ό κ²μ¦ν©λλ€.
Request:
message CompareWithStoredImageRequest {
string image_key = 1; // μ μ΄λ―Έμ§ ν€ (NCP κ²½λ‘: nose-print-photo/{petDID}/{fileName})
string pet_did = 2; // λΉκ΅ν Pet DID
}Response:
message CompareVectorsResponse {
float similarity = 1; // μ’
ν© μ μ¬λ μ μ (0.0 ~ 1.0)
float cosine_similarity = 2; // μ½μ¬μΈ μ μ¬λ (0.0 ~ 1.0)
float euclidean_distance = 3; // μ ν΄λ¦¬λ 거리
bool success = 4; // μ±κ³΅ μ¬λΆ
string error_message = 5; // μλ¬ λ©μμ§
int32 vector_size = 6; // λ²‘ν° μ°¨μ
optional MLErrorCode error_code = 7;
optional bool retryable = 8;
}μ μ¬λ μκ³κ°:
similarity >= 0.85: λμΌ ν«μΌλ‘ μΈμ similarity >= 0.70: μΆκ° κ²μ¦ νμsimilarity < 0.70: λ€λ₯Έ ν«μΌλ‘ νμ
Request:
message HealthCheckRequest {
string service = 1;
}Response:
message HealthCheckResponse {
enum ServingStatus {
UNKNOWN = 0;
SERVING = 1;
NOT_SERVING = 2;
SERVICE_UNKNOWN = 3;
}
ServingStatus status = 1; // μλΉμ€ μν
string message = 2; // μν λ©μμ§
string model_loaded = 3; // λͺ¨λΈ λ‘λ μν ("true" / "false")
string timestamp = 4; // νμμ€ν¬ν
}| μ½λ | Enum | μ€λͺ |
|---|---|---|
| ML_4001 | INVALID_IMAGE | μ ν¨νμ§ μμ μ΄λ―Έμ§ |
| ML_4002 | IMAGE_TOO_LARGE | μ΄λ―Έμ§ ν¬κΈ° μ΄κ³Ό |
| ML_4003 | INVALID_IMAGE_FORMAT | μ§μνμ§ μλ μ΄λ―Έμ§ νμ |
| ML_4004 | VECTOR_NOT_FOUND | λ²‘ν° λ°μ΄ν° μμ |
| ML_4005 | VECTOR_DIMENSION_MISMATCH | λ²‘ν° μ°¨μ λΆμΌμΉ |
| ML_4006 | INVALID_REQUEST | μλͺ»λ μμ² |
| μ½λ | Enum | μ€λͺ |
|---|---|---|
| ML_5001 | MODEL_NOT_LOADED | λͺ¨λΈ λ―Έλ‘λ |
| ML_5002 | INFERENCE_ERROR | μΆλ‘ μ€λ₯ |
| ML_5003 | STORAGE_CONNECTION_ERROR | μ€ν λ¦¬μ§ μ°κ²° μ€λ₯ |
| ML_5004 | INTERNAL_SERVER_ERROR | λ΄λΆ μλ² μ€λ₯ |
| ML_5005 | SERVICE_UNAVAILABLE | μλΉμ€ λΆκ° |
1. ν΄λΌμ΄μΈνΈκ° μ½ μ΄λ―Έμ§ 촬μ (2μ₯)
β
βΌ
2. API Gateway β ML Server: ExtractNoseVector()
β
βΌ
3. ML Server: μ΄λ―Έμ§ μ μ²λ¦¬ (224x224 리μ¬μ΄μ¦, μ κ·ν)
β
βΌ
4. ML Server: ONNX λͺ¨λΈ μΆλ‘ β 512μ°¨μ νΉμ§ 벑ν°
β
βΌ
5. API Gateway: keccak256(vector) β Pet DID μμ±
β
βΌ
6. λ²‘ν° λ° μ΄λ―Έμ§λ₯Ό NCP Object Storageμ μ μ₯
- nose-print-photo/{petDID}/image_1.jpg
- nose-print-photo/{petDID}/image_2.jpg
- nose-print-photo/{petDID}/vector.npy
β
βΌ
7. λΈλ‘체μΈμ Pet DID λ±λ‘ (PetDIDRegistry)
1. ν΄λΌμ΄μΈνΈκ° μ μ½ μ΄λ―Έμ§ 촬μ
β
βΌ
2. μ΄λ―Έμ§λ₯Ό NCPμ μμ μ μ₯
- nose-print-photo/{petDID}/verify_{timestamp}.jpg
β
βΌ
3. API Gateway β ML Server: CompareWithStoredImage()
β
βΌ
4. ML Server: μ μ΄λ―Έμ§μμ λ²‘ν° μΆμΆ
β
βΌ
5. ML Server: NCPμμ μ μ₯λ λ²‘ν° λ‘λ
β
βΌ
6. ML Server: μ μ¬λ κ³μ°
- μ½μ¬μΈ μ μ¬λ
- μ ν΄λ¦¬λ 거리
- μ’
ν© μ μ¬λ μ μ
β
βΌ
7. similarity >= 0.85 β κ²μ¦ μ±κ³΅
similarity < 0.85 β κ²μ¦ μ€ν¨
1. μ 보νΈμκ° ν«μ μ½ μ΄λ―Έμ§ 촬μ
β
βΌ
2. CompareWithStoredImage()λ‘ λμΌ ν« νμΈ
β
βΌ
3. similarity >= 0.85 νμΈ
β
βΌ
4. λΈλ‘체μΈμμ μμ κΆ μ΄μ (changeController)
β
βΌ
5. κΈ°μ‘΄ VC 무ν¨ν, μ VC λ°κΈ
- Python 3.11+
- pip λλ poetry
- ONNX Runtime
- NCP Object Storage μ κ·Ό κΆν
pip install -r requirements.txtpython download_model.pypython generate_proto.pyuvicorn src.main:app --reload --host 0.0.0.0 --port 50052python -m grpc_tools.protoc -I./proto --python_out=./src --grpc_python_out=./src ./proto/nose_embedder.proto
python src/main.pydocker build -t ml-server .
docker run -p 50052:50052 ml-serverkubectl apply -f k8s/# Server
GRPC_PORT=50052
# Model
MODEL_PATH=./embedder_model.onnx
MODEL_INPUT_SIZE=224
# NCP Object Storage
NCP_ACCESS_KEY=your-access-key
NCP_SECRET_KEY=your-secret-key
NCP_BUCKET_NAME=dogcatpaw-ml
NCP_ENDPOINT=https://kr.object.ncloudstorage.com
# Database
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=your-password
DB_DATABASE=ml_server
# Logging
LOG_LEVEL=infodogcatpaw-ml-server/
βββ src/
β βββ __init__.py
β βββ main.py # gRPC μλ² μ§μ
μ
β βββ service.py # NoseEmbedderService ꡬν
β βββ model/
β β βββ __init__.py
β β βββ embedder.py # ONNX λͺ¨λΈ λνΌ
β β βββ preprocessor.py # μ΄λ―Έμ§ μ μ²λ¦¬
β βββ storage/
β β βββ __init__.py
β β βββ ncp_storage.py # NCP Object Storage ν΄λΌμ΄μΈνΈ
β βββ utils/
β β βββ __init__.py
β β βββ similarity.py # μ μ¬λ κ³μ° ν¨μ
β β βββ vector_utils.py # λ²‘ν° μ νΈλ¦¬ν°
β βββ proto/
β βββ nose_embedder_pb2.py
β βββ nose_embedder_pb2_grpc.py
βββ k8s/ # Kubernetes λ°°ν¬ μ€μ
βββ embedder_model.onnx # ONNX λͺ¨λΈ νμΌ
βββ download_model.py # λͺ¨λΈ λ€μ΄λ‘λ μ€ν¬λ¦½νΈ
βββ generate_proto.py # Proto μ½λ μμ± μ€ν¬λ¦½νΈ
βββ requirements.txt # Python μμ‘΄μ±
βββ Dockerfile
βββ BUILD.md
| νλͺ© | κ° |
|---|---|
| Input Shape | (1, 3, 224, 224) |
| Input Format | RGB, float32, normalized [0, 1] |
| Output Shape | (1, 512) |
| Output Format | float32 embedding vector |
| Model Size | ~50MB |
def preprocess(image: bytes) -> np.ndarray:
# 1. λ°μ΄νΈ β PIL Image
img = Image.open(io.BytesIO(image))
# 2. RGB λ³ν
img = img.convert("RGB")
# 3. 리μ¬μ΄μ¦ (224x224)
img = img.resize((224, 224), Image.LANCZOS)
# 4. numpy λ°°μ΄ λ³ν
arr = np.array(img, dtype=np.float32)
# 5. μ κ·ν [0, 255] β [0, 1]
arr = arr / 255.0
# 6. μ±λ μμ λ³κ²½ (HWC β CHW)
arr = arr.transpose(2, 0, 1)
# 7. λ°°μΉ μ°¨μ μΆκ°
arr = np.expand_dims(arr, axis=0)
return arr// API Gatewayμ NoseEmbedderProxyService
@Injectable()
export class NoseEmbedderProxyService implements OnModuleInit {
private noseService: NoseEmbedderServiceClient;
constructor(@Inject('ML_GRPC_SERVICE') private client: ClientGrpc) {}
onModuleInit() {
this.noseService = this.client.getService<NoseEmbedderServiceClient>('NoseEmbedderService');
}
async extractVector(imageData: Buffer): Promise<NoseVectorResponse> {
return firstValueFrom(
this.noseService.ExtractNoseVector({
image_data: imageData,
image_format: 'jpeg',
})
);
}
async compareWithStored(imageKey: string, petDID: string): Promise<CompareVectorsResponse> {
return firstValueFrom(
this.noseService.CompareWithStoredImage({
image_key: imageKey,
pet_did: petDID,
})
);
}
}MIT License