David Ko commited on
Commit
aa7ad0c
ยท
1 Parent(s): e0f9202

Add object detection vector search feature with UI and API endpoints

Browse files
Files changed (2) hide show
  1. api.py +209 -1
  2. frontend/build/object-detection-search.html +581 -0
api.py CHANGED
@@ -43,6 +43,7 @@ except Exception as e:
43
  # Vector DB ์ดˆ๊ธฐํ™”
44
  vector_db = None
45
  image_collection = None
 
46
  try:
47
  # ChromaDB ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™” (์ธ๋ฉ”๋ชจ๋ฆฌ DB)
48
  vector_db = chromadb.Client()
@@ -57,11 +58,19 @@ try:
57
  get_or_create=True
58
  )
59
 
 
 
 
 
 
 
 
60
  print("Vector DB initialized successfully")
61
  except Exception as e:
62
  print("Error initializing Vector DB:", e)
63
  vector_db = None
64
  image_collection = None
 
65
 
66
  # YOLOv8 model
67
  yolo_model = None
@@ -598,6 +607,200 @@ def add_to_collection():
598
  print(f"Error in add-to-collection API: {e}")
599
  return jsonify({"error": str(e)}), 500
600
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
601
  @app.route('/', defaults={'path': ''}, methods=['GET'])
602
  @app.route('/<path:path>', methods=['GET'])
603
  def serve_react(path):
@@ -610,7 +813,12 @@ def serve_react(path):
610
  @app.route('/similar-images', methods=['GET'])
611
  def similar_images_page():
612
  """Serve similar images search page"""
613
- return send_from_directory(app.static_folder, 'similar-images.html')
 
 
 
 
 
614
 
615
  @app.route('/api/status', methods=['GET'])
616
  def status():
 
43
  # Vector DB ์ดˆ๊ธฐํ™”
44
  vector_db = None
45
  image_collection = None
46
+ object_collection = None
47
  try:
48
  # ChromaDB ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™” (์ธ๋ฉ”๋ชจ๋ฆฌ DB)
49
  vector_db = chromadb.Client()
 
58
  get_or_create=True
59
  )
60
 
61
+ # ๊ฐ์ฒด ์ธ์‹ ๊ฒฐ๊ณผ ์ปฌ๋ ‰์…˜ ์ƒ์„ฑ
62
+ object_collection = vector_db.create_collection(
63
+ name="object_collection",
64
+ embedding_function=ef,
65
+ get_or_create=True
66
+ )
67
+
68
  print("Vector DB initialized successfully")
69
  except Exception as e:
70
  print("Error initializing Vector DB:", e)
71
  vector_db = None
72
  image_collection = None
73
+ object_collection = None
74
 
75
  # YOLOv8 model
76
  yolo_model = None
 
607
  print(f"Error in add-to-collection API: {e}")
608
  return jsonify({"error": str(e)}), 500
609
 
610
+ @app.route('/api/add-detected-objects', methods=['POST'])
611
+ def add_detected_objects():
612
+ """๊ฐ์ฒด ์ธ์‹ ๊ฒฐ๊ณผ๋ฅผ ๋ฒกํ„ฐ DB์— ์ถ”๊ฐ€ํ•˜๋Š” API"""
613
+ if clip_model is None or object_collection is None:
614
+ return jsonify({"error": "Image embedding model or vector DB not available"})
615
+
616
+ try:
617
+ # ์š”์ฒญ์—์„œ ์ด๋ฏธ์ง€์™€ ๊ฐ์ฒด ๊ฒ€์ถœ ๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ ์ถ”์ถœ
618
+ data = request.json
619
+
620
+ if not data or 'image' not in data or 'objects' not in data:
621
+ return jsonify({"error": "Missing image or objects data"})
622
+
623
+ # ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
624
+ image_data = data['image']
625
+ if image_data.startswith('data:image'):
626
+ image_data = image_data.split(',')[1]
627
+
628
+ image = Image.open(BytesIO(base64.b64decode(image_data))).convert('RGB')
629
+ image_width, image_height = image.size
630
+
631
+ # ์ด๋ฏธ์ง€ ID
632
+ image_id = data.get('imageId', str(uuid.uuid4()))
633
+
634
+ # ๊ฐ์ฒด ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
635
+ objects = data['objects']
636
+ object_ids = []
637
+ object_embeddings = []
638
+ object_metadatas = []
639
+
640
+ for obj in objects:
641
+ # ๊ฐ์ฒด ID ์ƒ์„ฑ
642
+ object_id = f"{image_id}_{str(uuid.uuid4())[:8]}"
643
+
644
+ # ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ์ •๋ณด ์ถ”์ถœ
645
+ bbox = obj.get('bbox', {})
646
+ x1 = bbox.get('x', 0)
647
+ y1 = bbox.get('y', 0)
648
+ width = bbox.get('width', 0)
649
+ height = bbox.get('height', 0)
650
+
651
+ # ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค๋ฅผ ์ด๋ฏธ์ง€ ์ขŒํ‘œ๋กœ ๋ณ€ํ™˜
652
+ x1_px = int(x1 * image_width)
653
+ y1_px = int(y1 * image_height)
654
+ width_px = int(width * image_width)
655
+ height_px = int(height * image_height)
656
+
657
+ # ๊ฐ์ฒด ์ด๋ฏธ์ง€ ์ž๋ฅด๊ธฐ
658
+ try:
659
+ object_image = image.crop((x1_px, y1_px, x1_px + width_px, y1_px + height_px))
660
+
661
+ # ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ
662
+ embedding = generate_image_embedding(object_image)
663
+ if embedding is None:
664
+ continue
665
+
666
+ # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ
667
+ metadata = {
668
+ "image_id": image_id,
669
+ "class": obj.get('class', ''),
670
+ "confidence": obj.get('confidence', 0),
671
+ "bbox": {
672
+ "x": x1,
673
+ "y": y1,
674
+ "width": width,
675
+ "height": height
676
+ }
677
+ }
678
+
679
+ object_ids.append(object_id)
680
+ object_embeddings.append(embedding)
681
+ object_metadatas.append(metadata)
682
+ except Exception as e:
683
+ print(f"Error processing object: {e}")
684
+ continue
685
+
686
+ # ๊ฐ์ฒด๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ
687
+ if not object_ids:
688
+ return jsonify({"error": "No valid objects to add"})
689
+
690
+ # ๊ฐ์ฒด๋“ค์„ DB์— ์ถ”๊ฐ€
691
+ object_collection.add(
692
+ ids=object_ids,
693
+ embeddings=object_embeddings,
694
+ metadatas=object_metadatas
695
+ )
696
+
697
+ return jsonify({
698
+ "success": True,
699
+ "image_id": image_id,
700
+ "object_count": len(object_ids),
701
+ "object_ids": object_ids
702
+ })
703
+
704
+ except Exception as e:
705
+ print(f"Error in add-detected-objects API: {e}")
706
+ return jsonify({"error": str(e)}), 500
707
+
708
+ @app.route('/api/search-similar-objects', methods=['POST'])
709
+ def search_similar_objects():
710
+ """์œ ์‚ฌํ•œ ๊ฐ์ฒด ๊ฒ€์ƒ‰ API"""
711
+ if clip_model is None or object_collection is None:
712
+ return jsonify({"error": "Image embedding model or vector DB not available"})
713
+
714
+ try:
715
+ # ์š”์ฒญ ๋ฐ์ดํ„ฐ ์ถ”์ถœ
716
+ data = request.json
717
+
718
+ if not data:
719
+ return jsonify({"error": "Missing request data"})
720
+
721
+ # ๊ฒ€์ƒ‰ ์œ ํ˜• ๊ฒฐ์ •
722
+ search_type = data.get('searchType', 'image')
723
+ n_results = int(data.get('nResults', 5)) # ๊ฒฐ๊ณผ ๊ฐœ์ˆ˜
724
+
725
+ query_embedding = None
726
+
727
+ if search_type == 'image' and 'image' in data:
728
+ # ์ด๋ฏธ์ง€๋กœ ๊ฒ€์ƒ‰ํ•˜๋Š” ๊ฒฝ์šฐ
729
+ image_data = data['image']
730
+ if image_data.startswith('data:image'):
731
+ image_data = image_data.split(',')[1]
732
+
733
+ image = Image.open(BytesIO(base64.b64decode(image_data))).convert('RGB')
734
+ query_embedding = generate_image_embedding(image)
735
+
736
+ elif search_type == 'object' and 'objectId' in data:
737
+ # ๊ฐ์ฒด ID๋กœ ๊ฒ€์ƒ‰ํ•˜๋Š” ๊ฒฝ์šฐ
738
+ object_id = data['objectId']
739
+ result = object_collection.get(ids=[object_id], include=["embeddings"])
740
+
741
+ if result and "embeddings" in result and len(result["embeddings"]) > 0:
742
+ query_embedding = result["embeddings"][0]
743
+
744
+ elif search_type == 'class' and 'className' in data:
745
+ # ํด๋ž˜์Šค ์ด๋ฆ„์œผ๋กœ ๊ฒ€์ƒ‰ํ•˜๋Š” ๊ฒฝ์šฐ
746
+ class_name = data['className']
747
+ filter_query = {"class": {"$eq": class_name}}
748
+
749
+ # ํด๋ž˜์Šค๋กœ ํ•„ํ„ฐ๋งํ•˜์—ฌ ๊ฒ€์ƒ‰
750
+ results = object_collection.query(
751
+ query_embeddings=None,
752
+ where=filter_query,
753
+ n_results=n_results,
754
+ include=["metadatas", "distances"]
755
+ )
756
+
757
+ return jsonify({
758
+ "success": True,
759
+ "searchType": "class",
760
+ "results": format_object_results(results)
761
+ })
762
+
763
+ else:
764
+ return jsonify({"error": "Invalid search parameters"})
765
+
766
+ if query_embedding is None:
767
+ return jsonify({"error": "Failed to generate query embedding"})
768
+
769
+ # ์œ ์‚ฌ๋„ ๊ฒ€์ƒ‰ ์‹คํ–‰
770
+ results = object_collection.query(
771
+ query_embeddings=[query_embedding],
772
+ n_results=n_results,
773
+ include=["metadatas", "distances"]
774
+ )
775
+
776
+ return jsonify({
777
+ "success": True,
778
+ "searchType": search_type,
779
+ "results": format_object_results(results)
780
+ })
781
+
782
+ except Exception as e:
783
+ print(f"Error in search-similar-objects API: {e}")
784
+ return jsonify({"error": str(e)}), 500
785
+
786
+ def format_object_results(results):
787
+ """๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํฌ๋งทํŒ…"""
788
+ formatted_results = []
789
+
790
+ if len(results['ids']) > 0 and len(results['ids'][0]) > 0:
791
+ for i, obj_id in enumerate(results['ids'][0]):
792
+ result_item = {
793
+ "id": obj_id,
794
+ "metadata": results['metadatas'][0][i] if 'metadatas' in results else {}
795
+ }
796
+
797
+ if 'distances' in results:
798
+ result_item["distance"] = float(results['distances'][0][i])
799
+
800
+ formatted_results.append(result_item)
801
+
802
+ return formatted_results
803
+
804
  @app.route('/', defaults={'path': ''}, methods=['GET'])
805
  @app.route('/<path:path>', methods=['GET'])
806
  def serve_react(path):
 
813
  @app.route('/similar-images', methods=['GET'])
814
  def similar_images_page():
815
  """Serve similar images search page"""
816
+ return send_from_directory('frontend/build', 'similar-images.html')
817
+
818
+ @app.route('/object-detection-search', methods=['GET'])
819
+ def object_detection_search_page():
820
+ """Serve object detection search page"""
821
+ return send_from_directory('frontend/build', 'object-detection-search.html')
822
 
823
  @app.route('/api/status', methods=['GET'])
824
  def status():
frontend/build/object-detection-search.html ADDED
@@ -0,0 +1,581 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>๊ฐ์ฒด ์ธ์‹ ๊ฒฐ๊ณผ ๋ฒกํ„ฐ ๊ฒ€์ƒ‰</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <style>
9
+ body {
10
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
11
+ padding: 20px;
12
+ background-color: #f8f9fa;
13
+ }
14
+ .container {
15
+ max-width: 1200px;
16
+ margin: 0 auto;
17
+ background-color: white;
18
+ padding: 20px;
19
+ border-radius: 10px;
20
+ box-shadow: 0 0 10px rgba(0,0,0,0.1);
21
+ }
22
+ .header {
23
+ margin-bottom: 30px;
24
+ text-align: center;
25
+ }
26
+ .upload-section, .detection-section, .search-section, .results-section {
27
+ margin-bottom: 30px;
28
+ padding: 20px;
29
+ border: 1px solid #dee2e6;
30
+ border-radius: 5px;
31
+ }
32
+ .canvas-container {
33
+ position: relative;
34
+ margin: 20px 0;
35
+ }
36
+ #imageCanvas {
37
+ border: 1px solid #ddd;
38
+ max-width: 100%;
39
+ }
40
+ .object-item {
41
+ margin-bottom: 10px;
42
+ padding: 10px;
43
+ border: 1px solid #e9ecef;
44
+ border-radius: 5px;
45
+ background-color: #f8f9fa;
46
+ }
47
+ .result-item {
48
+ display: flex;
49
+ margin-bottom: 20px;
50
+ padding: 15px;
51
+ border: 1px solid #e9ecef;
52
+ border-radius: 5px;
53
+ background-color: #f8f9fa;
54
+ }
55
+ .result-image {
56
+ width: 150px;
57
+ height: 150px;
58
+ object-fit: cover;
59
+ margin-right: 15px;
60
+ border: 1px solid #ddd;
61
+ }
62
+ .result-details {
63
+ flex-grow: 1;
64
+ }
65
+ .badge {
66
+ margin-right: 5px;
67
+ }
68
+ .nav-tabs {
69
+ margin-bottom: 20px;
70
+ }
71
+ .tab-content {
72
+ padding: 20px;
73
+ border: 1px solid #dee2e6;
74
+ border-top: none;
75
+ border-radius: 0 0 5px 5px;
76
+ }
77
+ .bbox-overlay {
78
+ position: absolute;
79
+ border: 2px solid;
80
+ background-color: rgba(255, 255, 255, 0.2);
81
+ pointer-events: none;
82
+ }
83
+ .bbox-label {
84
+ position: absolute;
85
+ background-color: rgba(0, 0, 0, 0.7);
86
+ color: white;
87
+ padding: 2px 6px;
88
+ border-radius: 3px;
89
+ font-size: 12px;
90
+ pointer-events: none;
91
+ }
92
+ .spinner-border {
93
+ width: 1rem;
94
+ height: 1rem;
95
+ margin-right: 0.5rem;
96
+ }
97
+ .loading {
98
+ display: none;
99
+ align-items: center;
100
+ margin-top: 10px;
101
+ }
102
+ </style>
103
+ </head>
104
+ <body>
105
+ <div class="container">
106
+ <div class="header">
107
+ <h1>๊ฐ์ฒด ์ธ์‹ ๊ฒฐ๊ณผ ๋ฒกํ„ฐ ๊ฒ€์ƒ‰</h1>
108
+ <p class="lead">์ด๋ฏธ์ง€์—์„œ ์ธ์‹๋œ ๊ฐ์ฒด๋ฅผ ๋ฒกํ„ฐ DB์— ์ €์žฅํ•˜๊ณ  ์œ ์‚ฌํ•œ ๊ฐ์ฒด๋ฅผ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.</p>
109
+ </div>
110
+
111
+ <ul class="nav nav-tabs" id="myTab" role="tablist">
112
+ <li class="nav-item" role="presentation">
113
+ <button class="nav-link active" id="detect-tab" data-bs-toggle="tab" data-bs-target="#detect" type="button" role="tab" aria-controls="detect" aria-selected="true">๊ฐ์ฒด ์ธ์‹ ๋ฐ ์ €์žฅ</button>
114
+ </li>
115
+ <li class="nav-item" role="presentation">
116
+ <button class="nav-link" id="search-tab" data-bs-toggle="tab" data-bs-target="#search" type="button" role="tab" aria-controls="search" aria-selected="false">๊ฐ์ฒด ๊ฒ€์ƒ‰</button>
117
+ </li>
118
+ </ul>
119
+
120
+ <div class="tab-content" id="myTabContent">
121
+ <!-- ๊ฐ์ฒด ์ธ์‹ ๋ฐ ์ €์žฅ ํƒญ -->
122
+ <div class="tab-pane fade show active" id="detect" role="tabpanel" aria-labelledby="detect-tab">
123
+ <div class="upload-section">
124
+ <h3>์ด๋ฏธ์ง€ ์—…๋กœ๋“œ</h3>
125
+ <div class="mb-3">
126
+ <input class="form-control" type="file" id="imageUpload" accept="image/*">
127
+ </div>
128
+ <button id="detectObjectsBtn" class="btn btn-primary" disabled>๊ฐ์ฒด ์ธ์‹ํ•˜๊ธฐ</button>
129
+ <div class="loading" id="detectLoading">
130
+ <div class="spinner-border text-primary" role="status"></div>
131
+ <span>๊ฐ์ฒด ์ธ์‹ ์ค‘...</span>
132
+ </div>
133
+ </div>
134
+
135
+ <div class="detection-section">
136
+ <h3>์ธ์‹ ๊ฒฐ๊ณผ</h3>
137
+ <div class="canvas-container">
138
+ <canvas id="imageCanvas"></canvas>
139
+ <!-- ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค์™€ ๋ผ๋ฒจ์€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค -->
140
+ </div>
141
+ <div id="detectedObjects" class="mt-3">
142
+ <p>์ธ์‹๋œ ๊ฐ์ฒด๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.</p>
143
+ </div>
144
+ <button id="saveToVectorDBBtn" class="btn btn-success mt-3" disabled>๋ฒกํ„ฐ DB์— ์ €์žฅํ•˜๊ธฐ</button>
145
+ <div class="loading" id="saveLoading">
146
+ <div class="spinner-border text-success" role="status"></div>
147
+ <span>๋ฒกํ„ฐ DB์— ์ €์žฅ ์ค‘...</span>
148
+ </div>
149
+ </div>
150
+ </div>
151
+
152
+ <!-- ๊ฐ์ฒด ๊ฒ€์ƒ‰ ํƒญ -->
153
+ <div class="tab-pane fade" id="search" role="tabpanel" aria-labelledby="search-tab">
154
+ <div class="search-section">
155
+ <h3>๊ฒ€์ƒ‰ ๋ฐฉ๋ฒ•</h3>
156
+ <div class="mb-3">
157
+ <select class="form-select" id="searchType">
158
+ <option value="image">์ด๋ฏธ์ง€๋กœ ๊ฒ€์ƒ‰</option>
159
+ <option value="class">ํด๋ž˜์Šค๋กœ ๊ฒ€์ƒ‰</option>
160
+ </select>
161
+ </div>
162
+
163
+ <div id="imageSearchSection">
164
+ <div class="mb-3">
165
+ <label for="searchImageUpload" class="form-label">๊ฒ€์ƒ‰ํ•  ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ</label>
166
+ <input class="form-control" type="file" id="searchImageUpload" accept="image/*">
167
+ </div>
168
+ </div>
169
+
170
+ <div id="classSearchSection" style="display: none;">
171
+ <div class="mb-3">
172
+ <label for="classNameInput" class="form-label">ํด๋ž˜์Šค ์ด๋ฆ„</label>
173
+ <input type="text" class="form-control" id="classNameInput" placeholder="์˜ˆ: person, car, dog ๋“ฑ">
174
+ </div>
175
+ </div>
176
+
177
+ <div class="mb-3">
178
+ <label for="resultCount" class="form-label">๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ˆ˜</label>
179
+ <input type="number" class="form-control" id="resultCount" min="1" max="20" value="5">
180
+ </div>
181
+
182
+ <button id="searchBtn" class="btn btn-primary">๊ฒ€์ƒ‰ํ•˜๊ธฐ</button>
183
+ <div class="loading" id="searchLoading">
184
+ <div class="spinner-border text-primary" role="status"></div>
185
+ <span>๊ฒ€์ƒ‰ ์ค‘...</span>
186
+ </div>
187
+ </div>
188
+
189
+ <div class="results-section">
190
+ <h3>๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ</h3>
191
+ <div id="searchResults">
192
+ <p>๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.</p>
193
+ </div>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ </div>
198
+
199
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
200
+ <script>
201
+ // ์ „์—ญ ๋ณ€์ˆ˜
202
+ let currentImage = null;
203
+ let detectedObjects = [];
204
+ let imageWidth = 0;
205
+ let imageHeight = 0;
206
+ const colors = ['#FF5733', '#33FF57', '#3357FF', '#F033FF', '#FF3333', '#33FFFF', '#FFFF33'];
207
+
208
+ // DOM ์š”์†Œ
209
+ const imageUpload = document.getElementById('imageUpload');
210
+ const detectObjectsBtn = document.getElementById('detectObjectsBtn');
211
+ const imageCanvas = document.getElementById('imageCanvas');
212
+ const ctx = imageCanvas.getContext('2d');
213
+ const detectedObjectsDiv = document.getElementById('detectedObjects');
214
+ const saveToVectorDBBtn = document.getElementById('saveToVectorDBBtn');
215
+ const searchType = document.getElementById('searchType');
216
+ const imageSearchSection = document.getElementById('imageSearchSection');
217
+ const classSearchSection = document.getElementById('classSearchSection');
218
+ const searchImageUpload = document.getElementById('searchImageUpload');
219
+ const classNameInput = document.getElementById('classNameInput');
220
+ const resultCount = document.getElementById('resultCount');
221
+ const searchBtn = document.getElementById('searchBtn');
222
+ const searchResults = document.getElementById('searchResults');
223
+
224
+ // ๋กœ๋”ฉ ํ‘œ์‹œ
225
+ const detectLoading = document.getElementById('detectLoading');
226
+ const saveLoading = document.getElementById('saveLoading');
227
+ const searchLoading = document.getElementById('searchLoading');
228
+
229
+ // ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์ฒ˜๋ฆฌ
230
+ imageUpload.addEventListener('change', function(e) {
231
+ const file = e.target.files[0];
232
+ if (!file) return;
233
+
234
+ const reader = new FileReader();
235
+ reader.onload = function(event) {
236
+ const img = new Image();
237
+ img.onload = function() {
238
+ // ์บ”๋ฒ„์Šค ํฌ๊ธฐ ์„ค์ •
239
+ imageWidth = img.width;
240
+ imageHeight = img.height;
241
+
242
+ // ์บ”๋ฒ„์Šค ํฌ๊ธฐ๋ฅผ ์ด๋ฏธ์ง€์— ๋งž๊ฒŒ ์กฐ์ •ํ•˜๋˜, ์ตœ๋Œ€ ๋„ˆ๋น„ ์ œํ•œ
243
+ const maxWidth = 800;
244
+ let displayWidth = img.width;
245
+ let displayHeight = img.height;
246
+
247
+ if (displayWidth > maxWidth) {
248
+ const ratio = maxWidth / displayWidth;
249
+ displayWidth = maxWidth;
250
+ displayHeight = displayHeight * ratio;
251
+ }
252
+
253
+ imageCanvas.width = displayWidth;
254
+ imageCanvas.height = displayHeight;
255
+
256
+ // ์ด๋ฏธ์ง€ ๊ทธ๋ฆฌ๊ธฐ
257
+ ctx.drawImage(img, 0, 0, displayWidth, displayHeight);
258
+
259
+ // ํ˜„์žฌ ์ด๋ฏธ์ง€ ์ €์žฅ
260
+ currentImage = event.target.result;
261
+
262
+ // ๊ฐ์ฒด ์ธ์‹ ๋ฒ„ํŠผ ํ™œ์„ฑํ™”
263
+ detectObjectsBtn.disabled = false;
264
+
265
+ // ์ด์ „ ๊ฐ์ฒด ์ธ์‹ ๊ฒฐ๊ณผ ์ดˆ๊ธฐํ™”
266
+ detectedObjects = [];
267
+ detectedObjectsDiv.innerHTML = '<p>์ธ์‹๋œ ๊ฐ์ฒด๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.</p>';
268
+ saveToVectorDBBtn.disabled = true;
269
+
270
+ // ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ์ œ๊ฑฐ
271
+ clearBoundingBoxes();
272
+ };
273
+ img.src = event.target.result;
274
+ };
275
+ reader.readAsDataURL(file);
276
+ });
277
+
278
+ // ๊ฐ์ฒด ์ธ์‹ ์ฒ˜๋ฆฌ
279
+ detectObjectsBtn.addEventListener('click', function() {
280
+ if (!currentImage) return;
281
+
282
+ // ๋กœ๋”ฉ ํ‘œ์‹œ
283
+ detectLoading.style.display = 'flex';
284
+ detectObjectsBtn.disabled = true;
285
+
286
+ // ๊ฐ์ฒด ์ธ์‹ API ํ˜ธ์ถœ
287
+ fetch('/api/detect', {
288
+ method: 'POST',
289
+ headers: {
290
+ 'Content-Type': 'application/json'
291
+ },
292
+ body: JSON.stringify({
293
+ image: currentImage,
294
+ model: 'yolo' // ๊ธฐ๋ณธ ๋ชจ๋ธ๋กœ YOLO ์‚ฌ์šฉ
295
+ })
296
+ })
297
+ .then(response => response.json())
298
+ .then(data => {
299
+ // ๋กœ๋”ฉ ์ˆจ๊ธฐ๊ธฐ
300
+ detectLoading.style.display = 'none';
301
+ detectObjectsBtn.disabled = false;
302
+
303
+ if (data.error) {
304
+ alert('๊ฐ์ฒด ์ธ์‹ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ' + data.error);
305
+ return;
306
+ }
307
+
308
+ // ์ธ์‹๋œ ๊ฐ์ฒด ์ €์žฅ
309
+ detectedObjects = data.objects || [];
310
+
311
+ // ๊ฒฐ๊ณผ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ
312
+ if (detectedObjects.length === 0) {
313
+ detectedObjectsDiv.innerHTML = '<p>์ธ์‹๋œ ๊ฐ์ฒด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</p>';
314
+ saveToVectorDBBtn.disabled = true;
315
+ return;
316
+ }
317
+
318
+ // ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ๊ทธ๋ฆฌ๊ธฐ
319
+ drawBoundingBoxes();
320
+
321
+ // ์ธ์‹๋œ ๊ฐ์ฒด ๋ชฉ๋ก ํ‘œ์‹œ
322
+ displayDetectedObjects();
323
+
324
+ // ๋ฒกํ„ฐ DB ์ €์žฅ ๋ฒ„ํŠผ ํ™œ์„ฑํ™”
325
+ saveToVectorDBBtn.disabled = false;
326
+ })
327
+ .catch(error => {
328
+ detectLoading.style.display = 'none';
329
+ detectObjectsBtn.disabled = false;
330
+ console.error('Error:', error);
331
+ alert('๊ฐ์ฒด ์ธ์‹ ์š”์ฒญ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.');
332
+ });
333
+ });
334
+
335
+ // ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ๊ทธ๋ฆฌ๊ธฐ ํ•จ์ˆ˜
336
+ function drawBoundingBoxes() {
337
+ // ์บ”๋ฒ„์Šค ํฌ๊ธฐ ๊ฐ€์ ธ์˜ค๊ธฐ
338
+ const canvasWidth = imageCanvas.width;
339
+ const canvasHeight = imageCanvas.height;
340
+
341
+ // ๊ธฐ์กด ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ์ œ๊ฑฐ
342
+ clearBoundingBoxes();
343
+
344
+ // ์บ”๋ฒ„์Šค ์ปจํ…Œ์ด๋„ˆ ๊ฐ€์ ธ์˜ค๊ธฐ
345
+ const canvasContainer = document.querySelector('.canvas-container');
346
+
347
+ // ๊ฐ ๊ฐ์ฒด์— ๋Œ€ํ•ด ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ๊ทธ๋ฆฌ๊ธฐ
348
+ detectedObjects.forEach((obj, index) => {
349
+ const bbox = obj.bbox;
350
+ const colorIndex = index % colors.length;
351
+
352
+ // ์ƒ๋Œ€ ์ขŒํ‘œ๋ฅผ ์บ”๋ฒ„์Šค ์ขŒํ‘œ๋กœ ๋ณ€ํ™˜
353
+ const x = bbox.x * canvasWidth;
354
+ const y = bbox.y * canvasHeight;
355
+ const width = bbox.width * canvasWidth;
356
+ const height = bbox.height * canvasHeight;
357
+
358
+ // ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ์š”์†Œ ์ƒ์„ฑ
359
+ const boxElement = document.createElement('div');
360
+ boxElement.className = 'bbox-overlay';
361
+ boxElement.style.left = `${x}px`;
362
+ boxElement.style.top = `${y}px`;
363
+ boxElement.style.width = `${width}px`;
364
+ boxElement.style.height = `${height}px`;
365
+ boxElement.style.borderColor = colors[colorIndex];
366
+
367
+ // ๋ผ๋ฒจ ์š”์†Œ ์ƒ์„ฑ
368
+ const labelElement = document.createElement('div');
369
+ labelElement.className = 'bbox-label';
370
+ labelElement.style.left = `${x}px`;
371
+ labelElement.style.top = `${y - 20}px`;
372
+ labelElement.style.backgroundColor = colors[colorIndex];
373
+ labelElement.textContent = `${obj.class} ${Math.round(obj.confidence * 100)}%`;
374
+
375
+ // ์š”์†Œ๋ฅผ ์บ”๋ฒ„์Šค ์ปจํ…Œ์ด๋„ˆ์— ์ถ”๊ฐ€
376
+ canvasContainer.appendChild(boxElement);
377
+ canvasContainer.appendChild(labelElement);
378
+ });
379
+ }
380
+
381
+ // ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ์ œ๊ฑฐ ํ•จ์ˆ˜
382
+ function clearBoundingBoxes() {
383
+ const overlays = document.querySelectorAll('.bbox-overlay, .bbox-label');
384
+ overlays.forEach(overlay => overlay.remove());
385
+ }
386
+
387
+ // ์ธ์‹๋œ ๊ฐ์ฒด ๋ชฉ๋ก ํ‘œ์‹œ ํ•จ์ˆ˜
388
+ function displayDetectedObjects() {
389
+ let html = '<h4>์ธ์‹๋œ ๊ฐ์ฒด ๋ชฉ๋ก:</h4><ul class="list-group">';
390
+
391
+ detectedObjects.forEach((obj, index) => {
392
+ const colorIndex = index % colors.length;
393
+ html += `
394
+ <li class="list-group-item object-item">
395
+ <div class="d-flex justify-content-between align-items-center">
396
+ <div>
397
+ <span class="badge bg-primary">${index + 1}</span>
398
+ <span class="badge" style="background-color: ${colors[colorIndex]}">${obj.class}</span>
399
+ <span>์‹ ๋ขฐ๋„: ${Math.round(obj.confidence * 100)}%</span>
400
+ </div>
401
+ </div>
402
+ </li>
403
+ `;
404
+ });
405
+
406
+ html += '</ul>';
407
+ detectedObjectsDiv.innerHTML = html;
408
+ }
409
+
410
+ // ๋ฒกํ„ฐ DB์— ์ €์žฅ ์ฒ˜๋ฆฌ
411
+ saveToVectorDBBtn.addEventListener('click', function() {
412
+ if (!currentImage || detectedObjects.length === 0) return;
413
+
414
+ // ๋กœ๋”ฉ ํ‘œ์‹œ
415
+ saveLoading.style.display = 'flex';
416
+ saveToVectorDBBtn.disabled = true;
417
+
418
+ // ๋ฒกํ„ฐ DB ์ €์žฅ API ํ˜ธ์ถœ
419
+ fetch('/api/add-detected-objects', {
420
+ method: 'POST',
421
+ headers: {
422
+ 'Content-Type': 'application/json'
423
+ },
424
+ body: JSON.stringify({
425
+ image: currentImage,
426
+ objects: detectedObjects
427
+ })
428
+ })
429
+ .then(response => response.json())
430
+ .then(data => {
431
+ // ๋กœ๋”ฉ ์ˆจ๊ธฐ๊ธฐ
432
+ saveLoading.style.display = 'none';
433
+ saveToVectorDBBtn.disabled = false;
434
+
435
+ if (data.error) {
436
+ alert('๋ฒกํ„ฐ DB ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ' + data.error);
437
+ return;
438
+ }
439
+
440
+ alert(`์„ฑ๊ณต์ ์œผ๋กœ ${data.object_count}๊ฐœ์˜ ๊ฐ์ฒด๋ฅผ ๋ฒกํ„ฐ DB์— ์ €์žฅํ–ˆ์Šต๋‹ˆ๋‹ค.`);
441
+ })
442
+ .catch(error => {
443
+ saveLoading.style.display = 'none';
444
+ saveToVectorDBBtn.disabled = false;
445
+ console.error('Error:', error);
446
+ alert('๋ฒกํ„ฐ DB ์ €์žฅ ์š”์ฒญ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.');
447
+ });
448
+ });
449
+
450
+ // ๊ฒ€์ƒ‰ ์œ ํ˜• ๋ณ€๊ฒฝ ์ฒ˜๋ฆฌ
451
+ searchType.addEventListener('change', function() {
452
+ if (this.value === 'image') {
453
+ imageSearchSection.style.display = 'block';
454
+ classSearchSection.style.display = 'none';
455
+ } else if (this.value === 'class') {
456
+ imageSearchSection.style.display = 'none';
457
+ classSearchSection.style.display = 'block';
458
+ }
459
+ });
460
+
461
+ // ๊ฒ€์ƒ‰ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์ฒ˜๋ฆฌ
462
+ searchImageUpload.addEventListener('change', function(e) {
463
+ const file = e.target.files[0];
464
+ if (!file) return;
465
+
466
+ const reader = new FileReader();
467
+ reader.onload = function(event) {
468
+ // ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ ์ €์žฅ
469
+ searchImageUpload.dataset.image = event.target.result;
470
+ };
471
+ reader.readAsDataURL(file);
472
+ });
473
+
474
+ // ๊ฒ€์ƒ‰ ์ฒ˜๋ฆฌ
475
+ searchBtn.addEventListener('click', function() {
476
+ // ๋กœ๋”ฉ ํ‘œ์‹œ
477
+ searchLoading.style.display = 'flex';
478
+ searchBtn.disabled = true;
479
+
480
+ // ๊ฒ€์ƒ‰ ์œ ํ˜•์— ๋”ฐ๋ผ ์š”์ฒญ ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ
481
+ const searchTypeValue = searchType.value;
482
+ const nResults = parseInt(resultCount.value) || 5;
483
+ let requestData = {
484
+ searchType: searchTypeValue,
485
+ nResults: nResults
486
+ };
487
+
488
+ if (searchTypeValue === 'image') {
489
+ const imageData = searchImageUpload.dataset.image;
490
+ if (!imageData) {
491
+ alert('๊ฒ€์ƒ‰ํ•  ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•ด์ฃผ์„ธ์š”.');
492
+ searchLoading.style.display = 'none';
493
+ searchBtn.disabled = false;
494
+ return;
495
+ }
496
+ requestData.image = imageData;
497
+ } else if (searchTypeValue === 'class') {
498
+ const className = classNameInput.value.trim();
499
+ if (!className) {
500
+ alert('๊ฒ€์ƒ‰ํ•  ํด๋ž˜์Šค ์ด๋ฆ„์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.');
501
+ searchLoading.style.display = 'none';
502
+ searchBtn.disabled = false;
503
+ return;
504
+ }
505
+ requestData.className = className;
506
+ }
507
+
508
+ // ๊ฒ€์ƒ‰ API ํ˜ธ์ถœ
509
+ fetch('/api/search-similar-objects', {
510
+ method: 'POST',
511
+ headers: {
512
+ 'Content-Type': 'application/json'
513
+ },
514
+ body: JSON.stringify(requestData)
515
+ })
516
+ .then(response => response.json())
517
+ .then(data => {
518
+ // ๋กœ๋”ฉ ์ˆจ๊ธฐ๊ธฐ
519
+ searchLoading.style.display = 'none';
520
+ searchBtn.disabled = false;
521
+
522
+ if (data.error) {
523
+ alert('๊ฒ€์ƒ‰ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ' + data.error);
524
+ return;
525
+ }
526
+
527
+ // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ‘œ์‹œ
528
+ displaySearchResults(data.results);
529
+ })
530
+ .catch(error => {
531
+ searchLoading.style.display = 'none';
532
+ searchBtn.disabled = false;
533
+ console.error('Error:', error);
534
+ alert('๊ฒ€์ƒ‰ ์š”์ฒญ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.');
535
+ });
536
+ });
537
+
538
+ // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ‘œ์‹œ ํ•จ์ˆ˜
539
+ function displaySearchResults(results) {
540
+ if (!results || results.length === 0) {
541
+ searchResults.innerHTML = '<p>๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</p>';
542
+ return;
543
+ }
544
+
545
+ let html = '<div class="row">';
546
+
547
+ results.forEach((result, index) => {
548
+ const metadata = result.metadata || {};
549
+ const distance = result.distance ? (1 - result.distance).toFixed(2) * 100 : 0;
550
+ const imageId = metadata.image_id || '';
551
+ const className = metadata.class || '';
552
+ const confidence = metadata.confidence ? Math.round(metadata.confidence * 100) : 0;
553
+ const bbox = metadata.bbox || {};
554
+
555
+ html += `
556
+ <div class="col-md-6 mb-3">
557
+ <div class="card">
558
+ <div class="card-body">
559
+ <h5 class="card-title">๊ฒฐ๊ณผ #${index + 1}</h5>
560
+ <div class="d-flex justify-content-between">
561
+ <span class="badge bg-primary">${className}</span>
562
+ <span class="badge bg-info">์œ ์‚ฌ๋„: ${distance}%</span>
563
+ </div>
564
+ <p class="card-text mt-2">
565
+ <small>๊ฐ์ฒด ID: ${result.id}</small><br>
566
+ <small>์ด๋ฏธ์ง€ ID: ${imageId}</small><br>
567
+ <small>์‹ ๋ขฐ๋„: ${confidence}%</small><br>
568
+ <small>์œ„์น˜: X=${(bbox.x * 100).toFixed(1)}%, Y=${(bbox.y * 100).toFixed(1)}%, W=${(bbox.width * 100).toFixed(1)}%, H=${(bbox.height * 100).toFixed(1)}%</small>
569
+ </p>
570
+ </div>
571
+ </div>
572
+ </div>
573
+ `;
574
+ });
575
+
576
+ html += '</div>';
577
+ searchResults.innerHTML = html;
578
+ }
579
+ </script>
580
+ </body>
581
+ </html>