import cv2 import numpy as np from typing import Dict, List, Tuple import logging logger = logging.getLogger(__name__) class ReferenceDataExtractor: """참조 이미지에서 Ground Truth 데이터 추출""" def __init__(self): self.detector = self._initialize_detector() def _initialize_detector(self): """객체 감지기 초기화""" # YOLO 또는 다른 객체 감지기 사용 try: import ultralytics # 더 강력한 YOLO 모델 사용 (yolov8s) return ultralytics.YOLO('yolov8s.pt') except ImportError: logger.warning("YOLO not available, using OpenCV detector") return None def extract_reference_data(self, image_path: str) -> Dict: """ 참조 이미지에서 객체 정보 추출 Args: image_path: 참조 이미지 경로 Returns: 추출된 객체 정보 """ image = cv2.imread(image_path) if image is None: raise ValueError(f"Cannot load image: {image_path}") # 객체 감지 수행 if self.detector: logger.info(f"YOLO 객체 감지 시작 - 이미지 크기: {image.shape}") results = self.detector(image) objects = self._parse_yolo_results(results) # 상세한 디버깅 로그 추가 logger.info(f"YOLO 원본 감지 결과: {len(results)}개") logger.info(f"confidence 필터링 후: {len(objects)}개 객체") # 감지된 객체들의 상세 정보 로그 for i, obj in enumerate(objects): logger.info(f" 객체 {i+1}: bbox={obj['bbox']}, confidence={obj['confidence']:.3f}, class={obj['class']}") if len(objects) == 0: logger.warning("YOLO에서 객체를 감지하지 못했습니다. OpenCV fallback 사용") objects = self._extract_with_opencv(image) logger.info(f"OpenCV fallback 결과: {len(objects)}개 객체") else: logger.info("YOLO 감지기 없음, OpenCV fallback 직접 사용") objects = self._extract_with_opencv(image) logger.info(f"OpenCV fallback 결과: {len(objects)}개 객체") return { 'objects': objects, 'bounding_boxes': [obj['bbox'] for obj in objects], 'class_labels': [obj['class'] for obj in objects], 'confidence_scores': [obj['confidence'] for obj in objects] } def _parse_yolo_results(self, results) -> List[Dict]: """YOLO 결과 파싱 (개선된 버전)""" objects = [] confidence_threshold = 0.001 # 극도로 낮은 threshold로 설정 (0.01 → 0.001) logger.info(f"YOLO 결과 파싱 시작 - confidence threshold: {confidence_threshold}") total_detections = 0 filtered_by_confidence = 0 filtered_by_size = 0 for result in results: boxes = result.boxes if boxes is not None: total_detections += len(boxes) for box in boxes: confidence = box.conf[0].cpu().numpy() # confidence 필터링 추가 if confidence >= confidence_threshold: x1, y1, x2, y2 = box.xyxy[0].cpu().numpy() class_id = int(box.cls[0].cpu().numpy()) # 바운딩 박스 크기 검증 (매우 관대한 필터) width = x2 - x1 height = y2 - y1 if width > 1 and height > 1: # 최소 크기 필터 매우 완화 (5 → 1) objects.append({ 'bbox': [x1, y1, x2, y2], 'confidence': float(confidence), 'class': class_id }) logger.debug(f" 감지된 객체: confidence={confidence:.3f}, size=({width:.1f}x{height:.1f}), class={class_id}") else: filtered_by_size += 1 logger.debug(f" 크기 필터링: confidence={confidence:.3f}, size=({width:.1f}x{height:.1f})") else: filtered_by_confidence += 1 logger.debug(f" confidence 필터링: {confidence:.3f} < {confidence_threshold}") logger.info(f"YOLO 파싱 결과: 총 {total_detections}개 감지, confidence 필터링 {filtered_by_confidence}개, 크기 필터링 {filtered_by_size}개, 최종 {len(objects)}개 객체") # 객체가 감지되지 않은 경우 더 낮은 threshold로 재시도 if len(objects) == 0: logger.warning("기본 threshold에서 객체 감지 실패, 더 낮은 threshold로 재시도") confidence_threshold = 0.0001 # 극도로 낮은 threshold로 재시도 for result in results: boxes = result.boxes if boxes is not None: for box in boxes: confidence = box.conf[0].cpu().numpy() if confidence >= confidence_threshold: x1, y1, x2, y2 = box.xyxy[0].cpu().numpy() class_id = int(box.cls[0].cpu().numpy()) width = x2 - x1 height = y2 - y1 if width > 0.5 and height > 0.5: # 극도로 낮은 크기 필터 objects.append({ 'bbox': [x1, y1, x2, y2], 'confidence': float(confidence), 'class': class_id }) return objects def _extract_with_opencv(self, image: np.ndarray) -> List[Dict]: """OpenCV를 사용한 개선된 객체 추출""" try: # 이미지 전처리 processed_image = self._preprocess_image(image) # 다중 방법으로 객체 탐지 objects = [] # 방법 1: 개선된 Canny 엣지 검출 objects.extend(self._detect_with_improved_canny(processed_image)) # 방법 2: 히스토그램 균등화 + 그래디언트 기반 탐지 objects.extend(self._detect_with_histogram_equalization(processed_image)) # 방법 3: 동적 임계값 기반 탐지 objects.extend(self._detect_with_dynamic_threshold(processed_image)) # 중복 제거 및 필터링 objects = self._filter_and_merge_objects(objects) return objects except Exception as e: logger.error(f"OpenCV 객체 추출 중 오류 발생: {str(e)}") # 오류 발생 시 기본 방법 사용 return self._extract_with_basic_opencv(image) def _preprocess_image(self, image: np.ndarray) -> np.ndarray: """이미지 전처리 (대비 개선, 노이즈 제거, 엣지 강화)""" # 그레이스케일 변환 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 히스토그램 균등화 equalized = cv2.equalizeHist(gray) # 가우시안 블러로 노이즈 제거 blurred = cv2.GaussianBlur(equalized, (5, 5), 0) # 대비 개선 clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) enhanced = clahe.apply(blurred) return enhanced def _detect_with_improved_canny(self, image: np.ndarray) -> List[Dict]: """개선된 Canny 엣지 검출""" objects = [] # 동적 임계값 계산 median = np.median(image) lower = int(max(0, 0.7 * median)) upper = int(min(255, 1.3 * median)) # Canny 엣지 검출 edges = cv2.Canny(image, lower, upper) # 모폴로지 연산으로 엣지 강화 kernel = np.ones((3, 3), np.uint8) edges = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel) # 컨투어 찾기 contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for contour in contours: area = cv2.contourArea(contour) if area > 500: # 최소 면적 필터링 x, y, w, h = cv2.boundingRect(contour) if w > 30 and h > 30: # 최소 크기 필터링 objects.append({ 'bbox': [x, y, x + w, y + h], 'confidence': 0.7, 'class': 0, 'method': 'improved_canny' }) return objects def _detect_with_histogram_equalization(self, image: np.ndarray) -> List[Dict]: """히스토그램 균등화 + 그래디언트 기반 탐지""" objects = [] # Sobel 그래디언트 계산 grad_x = cv2.Sobel(image, cv2.CV_64F, 1, 0, ksize=3) grad_y = cv2.Sobel(image, cv2.CV_64F, 0, 1, ksize=3) gradient_magnitude = np.sqrt(grad_x**2 + grad_y**2) # 그래디언트 크기 정규화 gradient_magnitude = np.uint8(255 * gradient_magnitude / np.max(gradient_magnitude)) # 동적 임계값 적용 threshold = np.mean(gradient_magnitude) + np.std(gradient_magnitude) binary = cv2.threshold(gradient_magnitude, threshold, 255, cv2.THRESH_BINARY)[1] # 모폴로지 연산 kernel = np.ones((5, 5), np.uint8) binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) binary = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) # 컨투어 찾기 contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for contour in contours: area = cv2.contourArea(contour) if area > 800: # 최소 면적 필터링 x, y, w, h = cv2.boundingRect(contour) if w > 40 and h > 40: # 최소 크기 필터링 objects.append({ 'bbox': [x, y, x + w, y + h], 'confidence': 0.75, 'class': 0, 'method': 'histogram_equalization' }) return objects def _detect_with_dynamic_threshold(self, image: np.ndarray) -> List[Dict]: """동적 임계값 기반 탐지""" objects = [] # Otsu 임계값 계산 threshold_value, binary = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 적응적 임계값도 시도 adaptive_binary = cv2.adaptiveThreshold( image, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2 ) # 두 결과를 결합 combined = cv2.bitwise_or(binary, adaptive_binary) # 모폴로지 연산 kernel = np.ones((3, 3), np.uint8) combined = cv2.morphologyEx(combined, cv2.MORPH_CLOSE, kernel) # 컨투어 찾기 contours, _ = cv2.findContours(combined, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for contour in contours: area = cv2.contourArea(contour) if area > 600: # 최소 면적 필터링 x, y, w, h = cv2.boundingRect(contour) if w > 35 and h > 35: # 최소 크기 필터링 objects.append({ 'bbox': [x, y, x + w, y + h], 'confidence': 0.65, 'class': 0, 'method': 'dynamic_threshold' }) return objects def _filter_and_merge_objects(self, objects: List[Dict]) -> List[Dict]: """중복 제거 및 객체 필터링""" if not objects: return [] # IoU 기반 중복 제거 filtered_objects = [] for obj in objects: is_duplicate = False for existing_obj in filtered_objects: if self._calculate_iou(obj['bbox'], existing_obj['bbox']) > 0.5: # 더 높은 신뢰도를 가진 객체 유지 if obj['confidence'] > existing_obj['confidence']: filtered_objects.remove(existing_obj) filtered_objects.append(obj) is_duplicate = True break if not is_duplicate: filtered_objects.append(obj) # 신뢰도 순으로 정렬 filtered_objects.sort(key=lambda x: x['confidence'], reverse=True) # 상위 5개 객체만 반환 return filtered_objects[:5] def _calculate_iou(self, bbox1: List[float], bbox2: List[float]) -> float: """IoU 계산""" x1_1, y1_1, x2_1, y2_1 = bbox1 x1_2, y1_2, x2_2, y2_2 = bbox2 # 교집합 영역 계산 x1_i = max(x1_1, x1_2) y1_i = max(y1_1, y1_2) x2_i = min(x2_1, x2_2) y2_i = min(y2_1, y2_2) if x2_i <= x1_i or y2_i <= y1_i: return 0.0 intersection = (x2_i - x1_i) * (y2_i - y1_i) area1 = (x2_1 - x1_1) * (y2_1 - y1_1) area2 = (x2_2 - x1_2) * (y2_2 - y1_2) union = area1 + area2 - intersection return intersection / union if union > 0 else 0.0 def _extract_with_basic_opencv(self, image: np.ndarray) -> List[Dict]: """기본 OpenCV 방법 (오류 발생 시 대체) - 개선된 버전""" gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 더 관대한 Canny 엣지 검출 (임계값 완화) edges = cv2.Canny(gray, 30, 100) # 50, 150 → 30, 100 # 컨투어 찾기 contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) objects = [] for contour in contours: area = cv2.contourArea(contour) if area > 500: # 최소 면적 필터링 완화 (1000 → 500) x, y, w, h = cv2.boundingRect(contour) if w > 20 and h > 20: # 최소 크기 필터링 완화 (50 → 20) objects.append({ 'bbox': [x, y, x + w, y + h], 'confidence': 0.3, # 더 낮은 신뢰도 (0.5 → 0.3) 'class': 0, 'method': 'basic_fallback' }) # 객체가 여전히 없는 경우 더 관대한 방법 시도 if len(objects) == 0: logger.warning("기본 OpenCV 방법으로 객체 감지 실패, 더 관대한 방법 시도") objects = self._extract_with_adaptive_opencv(image) return objects def _extract_with_adaptive_opencv(self, image: np.ndarray) -> List[Dict]: """적응적 OpenCV 방법 (더 관대한 임계값)""" gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 적응적 임계값 적용 adaptive_thresh = cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2 ) # 모폴로지 연산으로 노이즈 제거 kernel = np.ones((3, 3), np.uint8) cleaned = cv2.morphologyEx(adaptive_thresh, cv2.MORPH_CLOSE, kernel) # 컨투어 찾기 contours, _ = cv2.findContours(cleaned, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) objects = [] for contour in contours: area = cv2.contourArea(contour) if area > 200: # 매우 낮은 면적 임계값 (500 → 200) x, y, w, h = cv2.boundingRect(contour) if w > 10 and h > 10: # 매우 낮은 크기 임계값 (20 → 10) objects.append({ 'bbox': [x, y, x + w, y + h], 'confidence': 0.2, # 매우 낮은 신뢰도 (0.3 → 0.2) 'class': 0, 'method': 'adaptive_fallback' }) # 여전히 객체가 없는 경우 이미지 전체를 하나의 객체로 처리 if len(objects) == 0: logger.warning("모든 OpenCV 방법 실패, 이미지 전체를 하나의 객체로 처리") h, w = image.shape[:2] objects.append({ 'bbox': [0, 0, w, h], 'confidence': 0.1, # 최소 신뢰도 'class': 0, 'method': 'full_image_fallback' }) return objects def _create_enhanced_fallback_data(self, image_path: str) -> Dict: """향상된 fallback 데이터 생성 (객체 감지 실패 시)""" logger.info("향상된 fallback 데이터 생성 시작") # 이미지 로드 image = cv2.imread(image_path) if image is None: raise ValueError(f"Cannot load image: {image_path}") h, w = image.shape[:2] # 이미지를 여러 영역으로 분할하여 객체로 처리 objects = [] # 방법 1: 이미지를 4개 영역으로 분할 quarter_w, quarter_h = w // 2, h // 2 regions = [ [0, 0, quarter_w, quarter_h], # 좌상단 [quarter_w, 0, w, quarter_h], # 우상단 [0, quarter_h, quarter_w, h], # 좌하단 [quarter_w, quarter_h, w, h] # 우하단 ] for i, region in enumerate(regions): x1, y1, x2, y2 = region objects.append({ 'bbox': [x1, y1, x2, y2], 'confidence': 0.4, # 중간 신뢰도 'class': i % 3, # 클래스 다양화 'method': 'region_split' }) # 방법 2: 이미지 전체를 하나의 객체로도 추가 objects.append({ 'bbox': [0, 0, w, h], 'confidence': 0.3, 'class': 0, 'method': 'full_image' }) logger.info(f"향상된 fallback 데이터 생성 완료: {len(objects)}개 객체") return { 'objects': objects, 'bounding_boxes': [obj['bbox'] for obj in objects], 'class_labels': [obj['class'] for obj in objects], 'confidence_scores': [obj['confidence'] for obj in objects] } def create_unified_ground_truth(self, reference_image_path: str, model_3d_path: str = None) -> Dict: """ 표준화된 Ground Truth 데이터 생성 Args: reference_image_path: 참조 이미지 경로 model_3d_path: 3D 모델 경로 (선택사항) Returns: 표준화된 Ground Truth 데이터 """ try: # 참조 이미지에서 객체 정보 추출 reference_data = self.extract_reference_data(reference_image_path) # 객체 감지 결과 검증 및 개선 if len(reference_data['objects']) == 0: logger.warning("객체 감지 실패, 대체 Ground Truth 생성") reference_data = self._create_enhanced_fallback_data(reference_image_path) # 표준 메시 구조 생성 unified_ground_truth = { 'vertices': self._generate_standard_vertices(reference_data), 'faces': self._generate_standard_faces(reference_data), 'center': self._calculate_center(reference_data), 'scale': self._calculate_scale(reference_data), 'objects': reference_data['objects'], 'bounding_boxes': reference_data['bounding_boxes'], 'class_labels': reference_data['class_labels'], 'confidence_scores': reference_data['confidence_scores'], 'metadata': { 'source_image': reference_image_path, 'extraction_method': 'yolo' if self.detector else 'opencv', 'timestamp': self._get_timestamp(), 'version': '1.0', 'object_count': len(reference_data['objects']), 'fallback_used': len(reference_data['objects']) == 0 } } # 데이터 검증 self._validate_ground_truth_structure(unified_ground_truth) logger.info(f"Ground Truth 생성 완료: {len(reference_data['objects'])}개 객체, {len(unified_ground_truth['vertices'])}개 vertices") return unified_ground_truth except Exception as e: logger.error(f"Ground Truth 생성 중 오류 발생: {str(e)}") # 오류 발생 시 기본 구조 반환 return self._create_fallback_ground_truth(reference_image_path) def _generate_standard_vertices(self, reference_data: Dict) -> List[List[float]]: """표준 vertices 생성""" vertices = [] for bbox in reference_data['bounding_boxes']: x1, y1, x2, y2 = bbox # 바운딩 박스를 3D 정육면체로 변환 # Z축은 기본값으로 설정 z_min, z_max = 0.0, 1.0 # 8개 꼭짓점 생성 (정육면체) cube_vertices = [ [x1, y1, z_min], [x2, y1, z_min], [x2, y2, z_min], [x1, y2, z_min], # 하단 [x1, y1, z_max], [x2, y1, z_max], [x2, y2, z_max], [x1, y2, z_max] # 상단 ] vertices.extend(cube_vertices) # 객체가 없는 경우 기본 정육면체 생성 if not vertices: # 이미지 전체를 하나의 객체로 처리 logger.warning("객체 감지 실패. 이미지 전체를 Ground Truth로 사용") # 이미지 크기 기반 3D 박스 생성 image = cv2.imread(reference_image_path) if image is not None: h, w = image.shape[:2] # 이미지 크기에 비례한 3D 박스 생성 scale_factor = min(w, h) / 100.0 # 정규화 vertices = [ [0, 0, 0], [scale_factor, 0, 0], [scale_factor, scale_factor, 0], [0, scale_factor, 0], # 하단 [0, 0, scale_factor], [scale_factor, 0, scale_factor], [scale_factor, scale_factor, scale_factor], [0, scale_factor, scale_factor] # 상단 ] # bounding_boxes도 업데이트 reference_data['bounding_boxes'] = [[0, 0, w, h]] reference_data['objects'] = [{ 'bbox': [0, 0, w, h], 'confidence': 0.5, 'class': 0 }] else: # 이미지 로드 실패 시 기본값 vertices = [ [0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1] ] return vertices def _generate_standard_faces(self, reference_data: Dict) -> List[List[int]]: """표준 faces 생성""" faces = [] # 각 바운딩 박스마다 12개 삼각형 면 생성 (정육면체) for i, bbox in enumerate(reference_data['bounding_boxes']): base_idx = i * 8 # 각 객체마다 8개 꼭짓점 # 정육면체의 12개 면 (각 면은 2개 삼각형으로 구성) cube_faces = [ # 하단 면 [base_idx, base_idx+1, base_idx+2], [base_idx, base_idx+2, base_idx+3], # 상단 면 [base_idx+4, base_idx+7, base_idx+6], [base_idx+4, base_idx+6, base_idx+5], # 앞면 [base_idx, base_idx+4, base_idx+5], [base_idx, base_idx+5, base_idx+1], # 뒷면 [base_idx+2, base_idx+6, base_idx+7], [base_idx+2, base_idx+7, base_idx+3], # 왼쪽 면 [base_idx, base_idx+3, base_idx+7], [base_idx, base_idx+7, base_idx+4], # 오른쪽 면 [base_idx+1, base_idx+5, base_idx+6], [base_idx+1, base_idx+6, base_idx+2] ] faces.extend(cube_faces) # 객체가 없는 경우 기본 정육면체 면 생성 if not faces: faces = [ [0, 1, 2], [0, 2, 3], # 하단 [4, 7, 6], [4, 6, 5], # 상단 [0, 4, 5], [0, 5, 1], # 앞면 [2, 6, 7], [2, 7, 3], # 뒷면 [0, 3, 7], [0, 7, 4], # 왼쪽 [1, 5, 6], [1, 6, 2] # 오른쪽 ] return faces def _calculate_center(self, reference_data: Dict) -> List[float]: """중심점 계산""" if not reference_data['bounding_boxes']: return [0.5, 0.5, 0.5] # 기본 중심점 # 모든 바운딩 박스의 중심점 계산 centers = [] for bbox in reference_data['bounding_boxes']: x1, y1, x2, y2 = bbox center_x = (x1 + x2) / 2 center_y = (y1 + y2) / 2 center_z = 0.5 # Z축은 기본값 centers.append([center_x, center_y, center_z]) # 전체 중심점 계산 if centers: center = [sum(c[i] for c in centers) / len(centers) for i in range(3)] else: center = [0.5, 0.5, 0.5] return center def _calculate_scale(self, reference_data: Dict) -> List[float]: """스케일 계산""" if not reference_data['bounding_boxes']: return [1.0, 1.0, 1.0] # 기본 스케일 # 모든 바운딩 박스의 크기 계산 scales = [] for bbox in reference_data['bounding_boxes']: x1, y1, x2, y2 = bbox scale_x = abs(x2 - x1) scale_y = abs(y2 - y1) scale_z = 1.0 # Z축은 기본값 scales.append([scale_x, scale_y, scale_z]) # 평균 스케일 계산 if scales: scale = [sum(s[i] for s in scales) / len(scales) for i in range(3)] else: scale = [1.0, 1.0, 1.0] return scale def _validate_ground_truth_structure(self, ground_truth: Dict) -> bool: """Ground Truth 구조 검증""" try: # 필수 키 확인 required_keys = ['vertices', 'faces', 'objects', 'bounding_boxes'] for key in required_keys: if key not in ground_truth: logger.error(f"Ground Truth에 필수 키 '{key}'가 없습니다") return False # vertices 검증 vertices = ground_truth['vertices'] if len(vertices) < 3: logger.error(f"vertices 개수가 너무 적습니다: {len(vertices)}") return False # objects 검증 objects = ground_truth['objects'] if len(objects) == 0: logger.warning("Ground Truth에 객체가 없습니다") return False logger.info(f"Ground Truth 구조 검증 완료: {len(vertices)}개 vertices, {len(objects)}개 객체") return True except Exception as e: logger.error(f"Ground Truth 구조 검증 중 오류: {e}") return False def _create_fallback_ground_truth(self, reference_image_path: str) -> Dict: """오류 발생 시 대체 Ground Truth 생성""" logger.warning("대체 Ground Truth 생성 중...") return { 'vertices': [ [0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], # 하단 [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1] # 상단 ], 'faces': [ [0, 1, 2], [0, 2, 3], # 하단 [4, 7, 6], [4, 6, 5], # 상단 [0, 4, 5], [0, 5, 1], # 앞면 [2, 6, 7], [2, 7, 3], # 뒷면 [0, 3, 7], [0, 7, 4], # 왼쪽 [1, 5, 6], [1, 6, 2] # 오른쪽 ], 'center': [0.5, 0.5, 0.5], 'scale': [1.0, 1.0, 1.0], 'objects': [], 'bounding_boxes': [], 'class_labels': [], 'confidence_scores': [], 'metadata': { 'source_image': reference_image_path, 'extraction_method': 'fallback', 'timestamp': self._get_timestamp(), 'version': '1.0', 'error': 'fallback_created' } } def _get_timestamp(self) -> str: """현재 시간 스탬프 반환""" from datetime import datetime return datetime.now().isoformat()