diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..fe1e6baee2b1d1694a81c002cf4fc57904d7a153 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..d6af6b726f2191523e556226c617b02911be75c0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,56 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +alopecia/count_result/visualizations/vis_230219_A191_1.jpg filter=lfs diff=lfs merge=lfs -text +alopecia/count_result/visualizations/vis_230219_A191_1_test.jpg filter=lfs diff=lfs merge=lfs -text +alopecia/count_result/visualizations/vis_230219_A200_4.jpg filter=lfs diff=lfs merge=lfs -text +alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-3.jpg filter=lfs diff=lfs merge=lfs -text +alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-4.jpg filter=lfs diff=lfs merge=lfs -text +alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-5.jpg filter=lfs diff=lfs merge=lfs -text +alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-6.jpg filter=lfs diff=lfs merge=lfs -text +alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-7.jpg filter=lfs diff=lfs merge=lfs -text +alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-9.jpg filter=lfs diff=lfs merge=lfs -text +complete_pipeline_out/complete_pipeline_230219_A191_1.jpg filter=lfs diff=lfs merge=lfs -text +complete_pipeline_out/complete_pipeline_230219_A191_1_test.jpg filter=lfs diff=lfs merge=lfs -text +complete_pipeline_out/complete_pipeline_230219_A200_4.jpg filter=lfs diff=lfs merge=lfs -text +complete_pipeline_out/complete_pipeline_IMG_02C458E4D7CE-3.jpeg filter=lfs diff=lfs merge=lfs -text +complete_pipeline_out/complete_pipeline_IMG_02C458E4D7CE-4.jpeg filter=lfs diff=lfs merge=lfs -text +complete_pipeline_out/complete_pipeline_IMG_02C458E4D7CE-5.jpeg filter=lfs diff=lfs merge=lfs -text +complete_pipeline_out/complete_pipeline_IMG_02C458E4D7CE-6.jpeg filter=lfs diff=lfs merge=lfs -text +complete_pipeline_out/labeled_230219_A191_1.jpg filter=lfs diff=lfs merge=lfs -text +complete_pipeline_out/labeled_230219_A191_1_test.jpg filter=lfs diff=lfs merge=lfs -text +complete_pipeline_out/labeled_230219_A200_4.jpg filter=lfs diff=lfs merge=lfs -text +complete_pipeline_out/labeled_IMG_02C458E4D7CE-3.jpeg filter=lfs diff=lfs merge=lfs -text +complete_pipeline_out/labeled_IMG_02C458E4D7CE-4.jpeg filter=lfs diff=lfs merge=lfs -text +complete_pipeline_out/labeled_IMG_02C458E4D7CE-5.jpeg filter=lfs diff=lfs merge=lfs -text +complete_pipeline_out/labeled_IMG_02C458E4D7CE-6.jpeg filter=lfs diff=lfs merge=lfs -text +complete_pipeline_out/lines_mask_IMG_02C458E4D7CE-3.jpeg filter=lfs diff=lfs merge=lfs -text +complete_pipeline_out/lines_mask_IMG_02C458E4D7CE-5.jpeg filter=lfs diff=lfs merge=lfs -text +complete_pipeline_out/lines_mask_IMG_02C458E4D7CE-6.jpeg filter=lfs diff=lfs merge=lfs -text +complete_pipeline_out/merged_foreground_IMG_02C458E4D7CE-3.jpeg filter=lfs diff=lfs merge=lfs -text +complete_pipeline_out/merged_foreground_IMG_02C458E4D7CE-6.jpeg filter=lfs diff=lfs merge=lfs -text +datasets/data/230219_A191_1.jpg filter=lfs diff=lfs merge=lfs -text +datasets/data/230219_A191_1_test.jpg filter=lfs diff=lfs merge=lfs -text +datasets/data/IMG_02C458E4D7CE-3.jpeg filter=lfs diff=lfs merge=lfs -text +datasets/data/IMG_02C458E4D7CE-4.jpeg filter=lfs diff=lfs merge=lfs -text +datasets/data/IMG_02C458E4D7CE-5.jpeg filter=lfs diff=lfs merge=lfs -text +datasets/data/IMG_02C458E4D7CE-6.jpeg filter=lfs diff=lfs merge=lfs -text +datasets/data/IMG_02C458E4D7CE-7.jpeg filter=lfs diff=lfs merge=lfs -text +datasets/data/IMG_02C458E4D7CE-9.jpeg filter=lfs diff=lfs merge=lfs -text +prediction/ensemble_result/ensemble_val/IMG_02C458E4D7CE-5.jpg filter=lfs diff=lfs merge=lfs -text +prediction/ensemble_result/ensemble_val/IMG_02C458E4D7CE-7.jpg filter=lfs diff=lfs merge=lfs -text +prediction/ensemble_result/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-3.jpeg filter=lfs diff=lfs merge=lfs -text +prediction/ensemble_result/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-5.jpeg filter=lfs diff=lfs merge=lfs -text +prediction/ensemble_result/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-6.jpeg filter=lfs diff=lfs merge=lfs -text +prediction/ensemble_result/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-7.jpeg filter=lfs diff=lfs merge=lfs -text +prediction/ensemble_result/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-9.jpeg filter=lfs diff=lfs merge=lfs -text +prediction/sam_result/sam_val/230219_A191_1.jpg filter=lfs diff=lfs merge=lfs -text +prediction/sam_result/sam_val/230219_A191_1_test.jpg filter=lfs diff=lfs merge=lfs -text +prediction/sam_result/sam_val/230219_A200_4.jpg filter=lfs diff=lfs merge=lfs -text +prediction/sam_result/sam_val/IMG_02C458E4D7CE-5.jpg filter=lfs diff=lfs merge=lfs -text +prediction/sam_result/sam_val/IMG_02C458E4D7CE-7.jpg filter=lfs diff=lfs merge=lfs -text +prediction/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-3.jpeg filter=lfs diff=lfs merge=lfs -text +prediction/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-5.jpeg filter=lfs diff=lfs merge=lfs -text +prediction/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-6.jpeg filter=lfs diff=lfs merge=lfs -text +prediction/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-7.jpeg filter=lfs diff=lfs merge=lfs -text +prediction/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-9.jpeg filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..c33bddb81a127684d1da160e6609203b07fb956e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/Users/Admin/ScalpVision/sam_vit_h_4b8939.pth +/Users/Admin/ScalpVision/alopecia/alopecia_prediction.py +/Users/Admin/ScalpVision/datasets/old_data +DiffuseIT-M +etc +_pycache_/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..115a773acfd327105ffe3d2fe2ded052c6664e69 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +## ScalpPipeline (`pipeline.py`) + +Refactored pipeline combining all steps into a single class. + +### Usage +```bash +python pipeline.py +``` + +### Dependencies (Files) +The pipeline requires the following files and directories to exist: + +1. **Input Images**: + - `datasets/data/` : Directory containing input images (`.jpg`, `.jpeg`, `.png`). + +2. **Model Weights**: + - `segmentation/model/U2NET.pth` : Pre-trained U2NET model. + - `sam_vit_h_4b8939.pth` : SAM (Segment Anything Model) checkpoint (ViT-H). + +### Output +Results are saved in: +- `datasets/seg_train/` (U2NET masks) +- `prediction/sam_result/sam_val/` (SAM masks) +- `prediction/ensemble_result/ensemble_val/` (Ensemble masks) +- `alopecia/thickness_result/` (Thickness data & visualization) +- `alopecia/count_result/` (Hair count CSV & visualization) diff --git a/__pycache__/pipeline.cpython-313.pyc b/__pycache__/pipeline.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6532a660d9a2e8601ee40b1f41c89d59fd789f0a Binary files /dev/null and b/__pycache__/pipeline.cpython-313.pyc differ diff --git a/__pycache__/pipeline.cpython-39.pyc b/__pycache__/pipeline.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..660d6cdcd68f13bcf5974eeafd0b0f4f0c8bfb00 Binary files /dev/null and b/__pycache__/pipeline.cpython-39.pyc differ diff --git a/alopecia/.DS_Store b/alopecia/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..2d5fefce35901017f360914ffc3e38e2c97fa35e Binary files /dev/null and b/alopecia/.DS_Store differ diff --git a/alopecia/__init__.py b/alopecia/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/alopecia/calculate_hair_count.py b/alopecia/calculate_hair_count.py new file mode 100644 index 0000000000000000000000000000000000000000..aa9968258c46ed3d943f2138252ae5799e9e3244 --- /dev/null +++ b/alopecia/calculate_hair_count.py @@ -0,0 +1,317 @@ +import cv2 +import os +import argparse +import numpy as np +import pandas as pd +from tqdm import tqdm +import json +import csv + +# --- CẤU HÌNH ĐƯỜNG DẪN CỦA BẠN --- +DEFAULT_SEGMENT_FOLDER = "../prediction/ensemble_result/ensemble_val" +DEFAULT_ORIGINAL_FOLDER = "../datasets/data" +DEFAULT_SAVE_PATH = "alopecia/count_result" + +def load_segment_mask(img_path): + if not os.path.exists(img_path): return None + img_gray = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) + if img_gray is None: return None + kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) + binary_filtered = cv2.morphologyEx(img_gray, cv2.MORPH_OPEN, kernel) + # ensure binary (0/255) + _, binary_filtered = cv2.threshold(binary_filtered, 127, 255, cv2.THRESH_BINARY) + return binary_filtered + +def run_watershed_for_sep(binary_img, original_img, sep_factor): + """ + Chạy watershed cho 1 separation_factor, trả về markers (int32 array) + markers: -1 = boundary, 1 = background, >=2 = object labels (giữ tương tự structure cũ) + """ + # Distance transform + dist_transform = cv2.distanceTransform(binary_img, cv2.DIST_L2, 5) + # sure foreground (seeds) + _, sure_fg = cv2.threshold(dist_transform, sep_factor * dist_transform.max(), 255, 0) + sure_fg = np.uint8(sure_fg) + # sure background + kernel = np.ones((3,3), np.uint8) + sure_bg = cv2.dilate(binary_img, kernel, iterations=3) + # unknown + unknown = cv2.subtract(sure_bg, sure_fg) + # markers + ret, markers = cv2.connectedComponents(sure_fg) + markers = markers + 1 # background = 1 + markers[unknown == 255] = 0 + # prepare original color + if len(original_img.shape) == 2: + original_color = cv2.cvtColor(original_img, cv2.COLOR_GRAY2BGR) + else: + original_color = original_img.copy() + # watershed + markers_w = markers.copy().astype(np.int32) + cv2.watershed(original_color, markers_w) + # After watershed: boundary pixels labeled -1; keep it + return markers_w + +def apply_watershed_hierarchical(binary_img, original_img, min_area, min_aspect_ratio, min_length, + separation_factor=0.2, hierarchy_levels=3): + """ + Multi-level hierarchical watershed refinement. + - Chạy watershed ở nhiều mức (tăng dần separation_factor) + - Bắt đầu từ coarse level (nhỏ nhất), refine bằng cách thay parent bằng các children từ level sau + - Chỉ chấp nhận split nếu child components thỏa điều kiện hình thái để tránh over-segmentation + """ + # 0. chuẩn bị (distance transform dùng bên trong run_watershed_for_sep) + # tạo danh sách separation levels (từ nhẹ -> mạnh) + # scale factors: từ sep*0.7 -> sep*1.6 (tùy hierarchy_levels) + low = max(0.01, separation_factor * 0.7) + high = separation_factor * 1.6 + if hierarchy_levels <= 1: + sep_levels = [separation_factor] + else: + sep_levels = list(np.linspace(low, high, hierarchy_levels)) + + # 1. chạy watershed cho mọi level + markers_levels = [] + for s in sep_levels: + markers_levels.append(run_watershed_for_sep(binary_img, original_img, s)) + + # 2. Bắt đầu từ coarse (level 0), dần refine bằng level 1,2,... + current = markers_levels[0].copy().astype(np.int32) + # normalize: ensure background value 1, boundaries -1 + # We'll create new labels map 'cur_labels' where labels >=2 are objects + # start label id counter + next_label = int(current.max()) + 1 + + # helper: compute morphological properties for a mask + def region_props_from_mask(mask_uint8): + # returns a dict with area, major_axis(length), minor_axis, aspect_ratio, centroid + cnts, _ = cv2.findContours(mask_uint8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + props = [] + for cnt in cnts: + area = cv2.contourArea(cnt) + if area <= 0: + continue + if len(cnt) >= 5: + try: + (x, y), (MA, ma), angle = cv2.fitEllipse(cnt) + except: + MA = ma = 0 + x = y = 0 + else: + # approximate with bounding rect + x, y, w, h = cv2.boundingRect(cnt) + MA = max(w,h) + ma = min(w,h) + angle = 0 + minor = ma if ma > 0 else 1e-6 + aspect = float(max(MA, ma)) / (minor + 1e-6) + props.append({ + 'area': area, + 'major': max(MA, ma), + 'minor': minor, + 'aspect': aspect, + 'centroid': (float(x), float(y)) if 'x' in locals() else (0,0), + 'contour': cnt + }) + return props + + # iterate refinement levels + for lvl in range(1, len(markers_levels)): + finer = markers_levels[lvl] + new_current = current.copy() + unique_parents = np.unique(current) + # ignore background and boundary labels + for parent_label in unique_parents: + if parent_label <= 1: # 1 background, 0/ -1 unknown/boundary + continue + parent_mask = (current == parent_label) + if parent_mask.sum() == 0: + continue + # find overlapping child labels in finer segmentation (exclude background/boundary) + overlapped = finer[parent_mask] + child_labels = np.unique(overlapped[(overlapped > 1)]) # only valid object labels + if len(child_labels) <= 1: + # no split present in finer-> keep parent + continue + + # Evaluate each child component restricted to parent mask + accepted_children = [] + for cl in child_labels: + child_mask = np.logical_and(finer == cl, parent_mask) + child_mask_uint8 = (child_mask.astype(np.uint8) * 255) + props = region_props_from_mask(child_mask_uint8) + if len(props) == 0: + continue + # take the largest prop if multiple contours + p = max(props, key=lambda x: x['area']) + # morphological filters + if p['area'] >= min_area and p['major'] >= min_length and p['aspect'] >= min_aspect_ratio: + accepted_children.append((child_mask_uint8, p)) + # Decide whether to replace parent by children + if len(accepted_children) >= 2: + # remove parent label and assign new labels for each accepted child + new_current[parent_mask] = 0 # clear parent region + for (cmask_uint8, p) in accepted_children: + new_label = next_label + next_label += 1 + new_current[cmask_uint8 == 255] = new_label + # else keep parent as-is (not split) + # after processing all parents, set current to new_current + # fill any 0 inside the binary foreground (remaining unknown areas) with background=1 to avoid gaps + # but keep watershed boundaries (-1) from original finer? We will not carry -1 boundaries forward. + current = new_current + # optional: small postprocess: fill small holes inside current using morphological close + # (skip for now to keep deterministic) + + # final_labels in 'current' (labels >=2 are hair candidates) + final_labels = current + + # At the end, extract contours for each final label and apply ellipse fitting + final filters + valid_hairs = [] + unique_labels = np.unique(final_labels) + for label in unique_labels: + if label <= 1: continue + mask = (final_labels == label).astype(np.uint8) * 255 + cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + for cnt in cnts: + area = cv2.contourArea(cnt) + if area < min_area: + continue + if len(cnt) < 5: + continue + try: + (x, y), (MA, ma), angle = cv2.fitEllipse(cnt) + major_axis = max(MA, ma) + minor_axis = min(MA, ma) + aspect_ratio = major_axis / (minor_axis + 1e-6) + if major_axis >= min_length and aspect_ratio >= min_aspect_ratio: + valid_hairs.append({ + 'centroid': (x, y), + 'ellipse': ((x, y), (MA, ma), angle), + 'length': major_axis, + 'thickness': minor_axis, + 'area': area, + 'label': int(label) + }) + except Exception: + continue + + return len(valid_hairs), valid_hairs + +def create_visualization(true_original, sam_background, hair_info, filename, save_dir): + h, w = true_original.shape[:2] + overlay = sam_background.copy() + if overlay.shape[:2] != (h, w): + overlay = cv2.resize(overlay, (w, h), interpolation=cv2.INTER_LINEAR) + for i, info in enumerate(hair_info): + cv2.ellipse(overlay, info['ellipse'], (0, 255, 0), 2) + cx, cy = map(int, info['centroid']) + if w > 300: + cv2.putText(overlay, str(i), (cx, cy), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255), 1) + border = np.zeros((h, 5, 3), dtype=np.uint8) + combined = np.hstack([true_original, border, overlay]) + header_height = 50 + header = np.zeros((header_height, combined.shape[1], 3), dtype=np.uint8) + info_text = f"{filename} | Count: {len(hair_info)}" + cv2.putText(header, info_text, (10, 35), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2) + final_vis = np.vstack([header, combined]) + cv2.imwrite(os.path.join(save_dir, f'vis_{filename}'), final_vis) + +def main(args, im): + segment_path = os.path.join(args.img_folder, im) + original_path = os.path.join(args.original_folder, im) + sam_path = os.path.join(args.sam_folder, im) + if not os.path.exists(segment_path): return 0, {} + binary = load_segment_mask(segment_path) + if binary is None: return 0, {} + true_original = cv2.imread(original_path) + if true_original is None: + true_original = np.zeros((binary.shape[0], binary.shape[1], 3), dtype=np.uint8) + sam_background = cv2.imread(sam_path) + if sam_background is None: + sam_background = cv2.cvtColor(binary, cv2.COLOR_GRAY2BGR) + + hair_count, hair_info = apply_watershed_hierarchical( + binary, + true_original, + min_area=args.min_area, + min_aspect_ratio=args.min_ratio, + min_length=args.min_length, + separation_factor=args.separation_factor, + hierarchy_levels=args.hierarchy_levels + ) + + density_data = { + 'count': hair_count, + 'avg_thickness': float(np.mean([h['thickness'] for h in hair_info]) if hair_info else 0), + 'avg_length': float(np.mean([h['length'] for h in hair_info]) if hair_info else 0) + } + + if args.draw_lines == 1: + vis_dir = os.path.join(args.save_path, 'visualizations') + os.makedirs(vis_dir, exist_ok=True) + create_visualization(true_original, sam_background, hair_info, im, vis_dir) + + return hair_count, density_data + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('--img_folder', type=str, default=DEFAULT_SEGMENT_FOLDER) + parser.add_argument('--original_folder', type=str, default=DEFAULT_ORIGINAL_FOLDER) + parser.add_argument('--sam_folder', type=str, default=DEFAULT_SEGMENT_FOLDER, + help="Thư mục chứa ảnh SAM segment (Visual bên phải).") + parser.add_argument('--save_path', type=str, default=DEFAULT_SAVE_PATH) + parser.add_argument('--label_csv', type=str, default="alopecia.csv") + parser.add_argument('--min_area', type=int, default=1500, help="Diện tích tối thiểu (pixels).") + parser.add_argument('--min_length', type=int, default=20, help="Chiều dài tối thiểu (pixels).") + parser.add_argument('--min_ratio', type=float, default=1.0, help="Tỷ lệ Dài/Rộng tối thiểu.") + parser.add_argument('--separation_factor', type=float, default=0.3, + help="Độ mạnh khi tách tóc dính (0.1-0.9). Cao = Tách nhiều.") + parser.add_argument('--hierarchy_levels', type=int, default=2, + help="Số mức hierarchical (multi-level) để refine segmentation. 1 = no hierarchy.") + parser.add_argument('--start', type=int, default=0) + parser.add_argument('--end', type=int, default=20000) + parser.add_argument('--draw_lines', type=int, default=1) + args = parser.parse_args() + + print(f"🚀 Bắt đầu đếm tóc (Hierarchical Watershed).") + print(f" separation_factor: {args.separation_factor}, hierarchy_levels: {args.hierarchy_levels}") + os.makedirs(args.save_path, exist_ok=True) + + img_names = [] + if os.path.exists(args.label_csv): + try: + df = pd.read_csv(args.label_csv) + img_names = df["img_name"].values[args.start:args.end] + except: + pass + if len(img_names) == 0: + import glob + for ext in ['*.jpg', '*.png', '*.jpeg']: + full_paths = glob.glob(os.path.join(args.img_folder, ext)) + img_names.extend([os.path.basename(p) for p in full_paths]) + if len(img_names) == 0: + img_names = ['230219_A191_1.jpg'] + + print(f"🔢 Số lượng ảnh cần xử lý: {len(img_names)}") + results = {} + density_results = {} + + for im in tqdm(img_names, desc="Processing"): + count, density = main(args, im) + if count > 0 or density: + results[im] = count + density_results[im] = density + + csv_path = os.path.join(args.save_path, 'hair_count.csv') + with open(csv_path, 'w', newline='') as f: + w = csv.writer(f) + w.writerow(['image_name', 'hair_count']) + for k, v in results.items(): + w.writerow([k, v]) + + json_path = os.path.join(args.save_path, 'density.json') + with open(json_path, 'w') as f: + json.dump(density_results, f, indent=2) + + print(f"✅ Hoàn thành! Visualizations tại: {os.path.join(args.save_path, 'visualizations')}") diff --git a/alopecia/calculate_hair_thickness.py b/alopecia/calculate_hair_thickness.py new file mode 100644 index 0000000000000000000000000000000000000000..c9979eea03ac0aab4cefb51f8a878770cb198bf7 --- /dev/null +++ b/alopecia/calculate_hair_thickness.py @@ -0,0 +1,174 @@ +import cv2 +import numpy as np +import glob +import argparse +from tqdm import tqdm +import os +import warnings +warnings.filterwarnings('ignore') + +def nms(boxes, thresh): + if len(boxes) == 0: + return [] + pick = [] + x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] + area = (x2 - x1 + 1) * (y2 - y1 + 1) + idxs = np.argsort(y2) + while len(idxs) > 0: + last = len(idxs) - 1 + i = idxs[last] + pick.append(i) + xx1 = np.maximum(x1[i], x1[idxs[:last]]) + yy1 = np.maximum(y1[i], y1[idxs[:last]]) + xx2 = np.minimum(x2[i], x2[idxs[:last]]) + yy2 = np.minimum(y2[i], y2[idxs[:last]]) + w = np.maximum(0, xx2 - xx1 + 1) + h = np.maximum(0, yy2 - yy1 + 1) + overlap = (w * h) / area[idxs[:last]] + idxs = np.delete(idxs, np.concatenate(([last], np.where(overlap > thresh)[0]))) + return boxes[pick] + +def find_pts_on_line(og, slope, d): + cx, cy = og + x1 = cx - d / ((1 + slope ** 2) ** 0.5) + y1 = cy - slope * cx + x1 * slope + if np.isnan(x1) or np.isnan(y1): + x1 = y1 = -1 + return x1, y1 + +def find_intersection_points2(center, slope, img, threshold): + p2 = p1 = (-1, -1) + w, h = img.shape + step, searching_len = 100, 50 + for d in range(1, step * searching_len): + px, py = find_pts_on_line(center, slope, d / step) + if (0 < int(px) < h) and (0 < int(py) < w) and img[int(py)][int(px)] > threshold: + p1 = (px, py) + else: + break + for d in range(1, step * searching_len): + px, py = find_pts_on_line(center, slope, -d / step) + if (0 < int(px) < h) and (0 < int(py) < w) and img[int(py)][int(px)] > threshold: + p2 = (px, py) + else: + break + dst = 0 if p1 == (-1, -1) or p2 == (-1, -1) else np.linalg.norm(np.asarray(p1) - np.asarray(p2)) + return [p1, p2], dst + +def get_direction2(bbox_pixels): + nonzero_indices = np.column_stack(np.nonzero(bbox_pixels)) + nonzero_indices = np.float32(nonzero_indices) + if len(nonzero_indices) >= 2: + mean, eigenvectors = cv2.PCACompute(nonzero_indices, mean=None) + cntr = ((mean[0, 1]), (mean[0, 0])) + return eigenvectors[0], cntr + else: + return (0,0), (0,0) + +def main(img_path, save_path, save_img=True): + img = cv2.imread(img_path) + imgray = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) + img_name = os.path.splitext(os.path.basename(img_path))[0] + + if np.all(imgray == 255) or np.all(imgray == 0): + np.save(os.path.join(save_path, img_name), np.array([])) + return + + ret, binary_map = cv2.threshold(imgray, 127, 255, 0) + nlabels, labels, stats, centroids = cv2.connectedComponentsWithStats(binary_map, None, None, None, 8, cv2.CV_32S) + areas = stats[1:, cv2.CC_STAT_AREA] + result = np.zeros((labels.shape), np.uint8) + for i in range(nlabels - 1): + if areas[i] >= 250: + result[labels == i + 1] = 255 + re_copy = result.copy() + + # skeletonization + skel = np.zeros(result.shape, np.uint8) + element = cv2.getStructuringElement(cv2.MORPH_CROSS, (3,3)) + while True: + open = cv2.morphologyEx(result, cv2.MORPH_OPEN, element) + temp = cv2.subtract(result, open) + eroded = cv2.erode(result, element) + skel = cv2.bitwise_or(skel, temp) + result = eroded.copy() + if cv2.countNonZero(result) == 0: + break + + # remove small skeleton noise + nlabels, labels, stats, centroids = cv2.connectedComponentsWithStats(skel, None, None, None, 8, cv2.CV_32S) + areas = stats[1:, cv2.CC_STAT_AREA] + skel = np.zeros((labels.shape), np.uint8) + for i in range(nlabels - 1): + if areas[i] >= 5: + skel[labels == i + 1] = 255 + + filtered_image = cv2.cvtColor(re_copy, cv2.COLOR_GRAY2BGR) + + # visualize skeleton + filtered_image[skel == 255] = [0, 255, 0] + + # bounding box extraction + white_pixels = np.where(skel == 255) + x_coords, y_coords = white_pixels[1], white_pixels[0] + filter_size = (20, 20) + x1, y1 = x_coords - filter_size[0]//2, y_coords - filter_size[1]//2 + x2, y2 = x_coords + filter_size[0]//2, y_coords + filter_size[1]//2 + white_regions = np.column_stack((x1, y1, x2, y2)) + white_regions = nms(white_regions, thresh=0.1) + + directions, center_points, thicknesses = [], [], [] + + for coor in white_regions: + x1, y1, x2, y2 = coor + bbox_pixels = skel[y1:y2, x1:x2] + direction, mean = get_direction2(bbox_pixels) + directions.append(direction) + center_points.append((mean[0] + x1, mean[1] + y1)) + + perpendicular_slope = [] + for direction in directions: + if direction[1] != 0: + perpendicular_slope.append(-1 / (direction[0] / direction[1])) + else: + perpendicular_slope.append(0) + + for center_point, perp_slope in zip(center_points, perpendicular_slope): + intersection, dst = find_intersection_points2(center_point, perp_slope, re_copy, 200) + if dst != 0: + thicknesses.append(dst) + if intersection[0] != (-1, -1) and intersection[1] != (-1, -1): + cv2.line(filtered_image, + (int(intersection[0][0]), int(intersection[0][1])), + (int(intersection[1][0]), int(intersection[1][1])), + (0, 255, 255), 1) + for pt in intersection: + cv2.circle(filtered_image, (int(pt[0]), int(pt[1])), 3, (0, 0, 255), -1) + + # display text + if len(thicknesses) > 0: + avg_thickness = np.mean(thicknesses) + cv2.putText(filtered_image, f"Avg thickness: {avg_thickness:.2f}px", + (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 0, 0), 2) + + if save_img: + #save_img_path = os.path.join(save_path, f"{img_name}_vis.jpg") + save_img_path = os.path.join(save_path, f"{img_name}_vis.png") + + cv2.imwrite(save_img_path, filtered_image) + + np.save(os.path.join(save_path, img_name), np.sort(thicknesses)) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--img_folder', type=str, default='prediction/ensemble_result/ensemble_val') + #parser.add_argument('--img_folder', type=str, default='datasets/seg_train/') + parser.add_argument('--save_path', type=str, default='alopecia/thickness_result') + args = parser.parse_args() + + os.makedirs(args.save_path, exist_ok=True) + for im in tqdm(sorted(glob.glob(os.path.join(args.img_folder, '*.jpg')))): + #for im in tqdm(sorted(glob.glob(os.path.join(args.img_folder, '*.png')))): + + main(im, args.save_path, save_img=True) diff --git a/alopecia/count_result/.DS_Store b/alopecia/count_result/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..4ddfe73ccd409d404d73801cdde47248d2e94275 Binary files /dev/null and b/alopecia/count_result/.DS_Store differ diff --git a/alopecia/count_result/density.json b/alopecia/count_result/density.json new file mode 100644 index 0000000000000000000000000000000000000000..6f8245f17300519343f439dcef684c9b64119599 --- /dev/null +++ b/alopecia/count_result/density.json @@ -0,0 +1,47 @@ +{ + "test_step7_merged_foreground_230219_A200_4.jpg": { + "count": 8, + "avg_thickness": 46.929224491119385, + "avg_length": 450.0047779083252 + }, + "test_step7_merged_foreground_230219_A191_1_test.jpg": { + "count": 10, + "avg_thickness": 54.611060333251956, + "avg_length": 241.15121002197264 + }, + "test_step7_merged_foreground_230219_A191_1.jpg": { + "count": 10, + "avg_thickness": 54.45850448608398, + "avg_length": 238.0105888366699 + }, + "test_step7_merged_foreground_IMG_02C458E4D7CE-6.jpeg": { + "count": 14, + "avg_thickness": 51.27924510410854, + "avg_length": 175.85291181291853 + }, + "test_step7_merged_foreground_IMG_02C458E4D7CE-7.jpeg": { + "count": 8, + "avg_thickness": 54.27548885345459, + "avg_length": 192.99601650238037 + }, + "test_step7_merged_foreground_IMG_02C458E4D7CE-3.jpeg": { + "count": 8, + "avg_thickness": 61.52065181732178, + "avg_length": 262.84897232055664 + }, + "test_step7_merged_foreground_IMG_02C458E4D7CE-4.jpeg": { + "count": 10, + "avg_thickness": 56.64297752380371, + "avg_length": 114.51317901611328 + }, + "test_step7_merged_foreground_IMG_02C458E4D7CE-9.jpeg": { + "count": 11, + "avg_thickness": 57.8118123141202, + "avg_length": 165.69457591663706 + }, + "test_step7_merged_foreground_IMG_02C458E4D7CE-5.jpeg": { + "count": 11, + "avg_thickness": 47.383614626797765, + "avg_length": 199.2529823996804 + } +} \ No newline at end of file diff --git a/alopecia/count_result/hair_count.csv b/alopecia/count_result/hair_count.csv new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/alopecia/count_result/visualizations/vis_230219_A191_1.jpg b/alopecia/count_result/visualizations/vis_230219_A191_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9c9005904a25e721a66fca062b7e277b9a81bb8e --- /dev/null +++ b/alopecia/count_result/visualizations/vis_230219_A191_1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7aeae9f508d0c521bdcc0c0360887cb93a6146093c9539b99cbdd8968a752e6a +size 455679 diff --git a/alopecia/count_result/visualizations/vis_230219_A191_1_test.jpg b/alopecia/count_result/visualizations/vis_230219_A191_1_test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b3b00c0242ee1da186166e04344cbcb04c501da1 --- /dev/null +++ b/alopecia/count_result/visualizations/vis_230219_A191_1_test.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9954e4a36e820b1528491fd29e3fe9370164036c24b3badb814834fd15688aa +size 457070 diff --git a/alopecia/count_result/visualizations/vis_230219_A200_4.jpg b/alopecia/count_result/visualizations/vis_230219_A200_4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6a2a58f2d79736a4586e809e682dbaeee85492e2 --- /dev/null +++ b/alopecia/count_result/visualizations/vis_230219_A200_4.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:faef0e7b12530e858eceec850dcf2242a72205ba7cd62aed7edf6c5a9cb83609 +size 413615 diff --git a/alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-3.jpg b/alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..020c1ae5a39e8fbf767094da9905bb19ed11d056 --- /dev/null +++ b/alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-3.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8526214c5f44aa2671a60153fb8996e84a3e18d000f296fdac78cbfd07e8c95f +size 191191 diff --git a/alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-4.jpg b/alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4d89f916558a62b033d534b4c4ec0c6ca2903d1c --- /dev/null +++ b/alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-4.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9bee74276e6e764ce5beef4330935bb8de5e5e4b2c58cbd54f879f2aea33bced +size 149822 diff --git a/alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-5.jpg b/alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..372c1a701e7d961f1868cde6033d9ee0af020f3d --- /dev/null +++ b/alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-5.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f76c4e3c434490870cf0b40b6ea8d38852c8c83f571cc6b72c0a26e579f001b +size 231669 diff --git a/alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-6.jpg b/alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ea2371d1d1431a17837bfc7b160f75418a58806b --- /dev/null +++ b/alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-6.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22c39d8dd31426a1d79f527aed84efb3f88313b1de210be02e6c7e2af9ae64fb +size 193412 diff --git a/alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-7.jpg b/alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-7.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f36a7b96fd975e8e4fa38c075d2bdd4de2832ac8 --- /dev/null +++ b/alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-7.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7367a9e68423daa9a8d4b6929957c208cb396ab4108b86ff55534af3883a5f9b +size 233155 diff --git a/alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-9.jpg b/alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-9.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2e88c7cf7894601296b831199e15e1dc78a3e40b --- /dev/null +++ b/alopecia/count_result/visualizations/vis_IMG_02C458E4D7CE-9.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:960e82b1f4891435dfea19dcb5cd2986fc4ebcf9912b846609c60b5fc197887d +size 172709 diff --git a/alopecia/thickness_result/230219_A191_1.npy b/alopecia/thickness_result/230219_A191_1.npy new file mode 100644 index 0000000000000000000000000000000000000000..60280cbbe975b4a9321c2c954c17a16c226ff96c --- /dev/null +++ b/alopecia/thickness_result/230219_A191_1.npy @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6f01b1d91d76255879fd6e524486b74cef38076ccf9996864338fec28e6f6ec +size 1488 diff --git a/alopecia/thickness_result/230219_A191_1_test.npy b/alopecia/thickness_result/230219_A191_1_test.npy new file mode 100644 index 0000000000000000000000000000000000000000..874ebc2d014efdd098b3f1e625e0b298427305e6 --- /dev/null +++ b/alopecia/thickness_result/230219_A191_1_test.npy @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:607cc28b956e255a4e54b6685e8124f85a76eaa14f2c19edb2d449f4bc9f0b5b +size 1512 diff --git a/alopecia/thickness_result/230219_A191_1_test_vis.png b/alopecia/thickness_result/230219_A191_1_test_vis.png new file mode 100644 index 0000000000000000000000000000000000000000..88ca1fcf540336a6dc03b7a52c9b3df5616bbba4 Binary files /dev/null and b/alopecia/thickness_result/230219_A191_1_test_vis.png differ diff --git a/alopecia/thickness_result/230219_A191_1_vis.png b/alopecia/thickness_result/230219_A191_1_vis.png new file mode 100644 index 0000000000000000000000000000000000000000..291eb93b66a73a31308434c28f91d990c1d16b41 Binary files /dev/null and b/alopecia/thickness_result/230219_A191_1_vis.png differ diff --git a/alopecia/thickness_result/230219_A200_4.npy b/alopecia/thickness_result/230219_A200_4.npy new file mode 100644 index 0000000000000000000000000000000000000000..b009f9db668c9ad12f1159c8e6ddb14562e45580 --- /dev/null +++ b/alopecia/thickness_result/230219_A200_4.npy @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2351d330483fec1d79006cb84670aa1ebe97f707e7a8b2b9034eccc0b951b256 +size 1744 diff --git a/alopecia/thickness_result/230219_A200_4_vis.png b/alopecia/thickness_result/230219_A200_4_vis.png new file mode 100644 index 0000000000000000000000000000000000000000..19ed379763bddcb7651d1672ebfd9241cef1d1d7 Binary files /dev/null and b/alopecia/thickness_result/230219_A200_4_vis.png differ diff --git a/alopecia/thickness_result/IMG_02C458E4D7CE-3.npy b/alopecia/thickness_result/IMG_02C458E4D7CE-3.npy new file mode 100644 index 0000000000000000000000000000000000000000..a575a3fb62f316128e1503e39de1b5af2752bd45 --- /dev/null +++ b/alopecia/thickness_result/IMG_02C458E4D7CE-3.npy @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:199859cfc87e9aa66a5285c5adcd910ec64be9382b24ee942fef10461417d816 +size 2368 diff --git a/alopecia/thickness_result/IMG_02C458E4D7CE-3_vis.png b/alopecia/thickness_result/IMG_02C458E4D7CE-3_vis.png new file mode 100644 index 0000000000000000000000000000000000000000..a25066be08f139982d97a05cd68d04b17a709362 Binary files /dev/null and b/alopecia/thickness_result/IMG_02C458E4D7CE-3_vis.png differ diff --git a/alopecia/thickness_result/IMG_02C458E4D7CE-4.npy b/alopecia/thickness_result/IMG_02C458E4D7CE-4.npy new file mode 100644 index 0000000000000000000000000000000000000000..c31f81edd9f6962b6931fc9ec38b5540df45e655 --- /dev/null +++ b/alopecia/thickness_result/IMG_02C458E4D7CE-4.npy @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f88234e63aac39ba4c7eb6f1c63ad10608af32531d34b3e5fdac8a0af7952d5f +size 1696 diff --git a/alopecia/thickness_result/IMG_02C458E4D7CE-4_vis.png b/alopecia/thickness_result/IMG_02C458E4D7CE-4_vis.png new file mode 100644 index 0000000000000000000000000000000000000000..5162732b76f315459a598d8e45a2decd0d439749 Binary files /dev/null and b/alopecia/thickness_result/IMG_02C458E4D7CE-4_vis.png differ diff --git a/alopecia/thickness_result/IMG_02C458E4D7CE-5.npy b/alopecia/thickness_result/IMG_02C458E4D7CE-5.npy new file mode 100644 index 0000000000000000000000000000000000000000..f5edc1b8d52dc5caf5c3bbc3f2a2161dd4a33a65 --- /dev/null +++ b/alopecia/thickness_result/IMG_02C458E4D7CE-5.npy @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56bd5bdebca503087087898b2bc2143f935c90ced8bd95b2c137251eb7b00a87 +size 2600 diff --git a/alopecia/thickness_result/IMG_02C458E4D7CE-5_vis.png b/alopecia/thickness_result/IMG_02C458E4D7CE-5_vis.png new file mode 100644 index 0000000000000000000000000000000000000000..5a0656eaa9ca5c9abec74dce2c2f3b5d84963dfb Binary files /dev/null and b/alopecia/thickness_result/IMG_02C458E4D7CE-5_vis.png differ diff --git a/alopecia/thickness_result/IMG_02C458E4D7CE-6.npy b/alopecia/thickness_result/IMG_02C458E4D7CE-6.npy new file mode 100644 index 0000000000000000000000000000000000000000..7dbd9e10b700bd6a943fe5e869274f94b7f40baf --- /dev/null +++ b/alopecia/thickness_result/IMG_02C458E4D7CE-6.npy @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6380a99045f6ff977c1fa03b5364ae84d66e1429b510353272813161573397ea +size 2608 diff --git a/alopecia/thickness_result/IMG_02C458E4D7CE-6_vis.png b/alopecia/thickness_result/IMG_02C458E4D7CE-6_vis.png new file mode 100644 index 0000000000000000000000000000000000000000..4d9c9a7869438f29099b428441e28a54241aa876 Binary files /dev/null and b/alopecia/thickness_result/IMG_02C458E4D7CE-6_vis.png differ diff --git a/alopecia/thickness_result/IMG_02C458E4D7CE-7.npy b/alopecia/thickness_result/IMG_02C458E4D7CE-7.npy new file mode 100644 index 0000000000000000000000000000000000000000..2e5a40a51db3571012b35521731ce771bd2a601e --- /dev/null +++ b/alopecia/thickness_result/IMG_02C458E4D7CE-7.npy @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4900a99a4b11a1041c48c204e2e9eb4aaa0bdb7bd2f14be84b8f6373003473b5 +size 3360 diff --git a/alopecia/thickness_result/IMG_02C458E4D7CE-7_vis.png b/alopecia/thickness_result/IMG_02C458E4D7CE-7_vis.png new file mode 100644 index 0000000000000000000000000000000000000000..eed5a20a0a556584096c9edda54f9a5d7554c33c Binary files /dev/null and b/alopecia/thickness_result/IMG_02C458E4D7CE-7_vis.png differ diff --git a/alopecia/thickness_result/IMG_02C458E4D7CE-9.npy b/alopecia/thickness_result/IMG_02C458E4D7CE-9.npy new file mode 100644 index 0000000000000000000000000000000000000000..3b52001020ada6f326a527e0eb99ba22e02075bc --- /dev/null +++ b/alopecia/thickness_result/IMG_02C458E4D7CE-9.npy @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1250d1867a193a28c301bba6f74cdd97bf549ebeb438bdc0e75ac560a2ac1a0 +size 1952 diff --git a/alopecia/thickness_result/IMG_02C458E4D7CE-9_vis.png b/alopecia/thickness_result/IMG_02C458E4D7CE-9_vis.png new file mode 100644 index 0000000000000000000000000000000000000000..ac87a3200db2ca6c5269d653c774bdffd9793748 Binary files /dev/null and b/alopecia/thickness_result/IMG_02C458E4D7CE-9_vis.png differ diff --git a/auto_run.py b/auto_run.py new file mode 100644 index 0000000000000000000000000000000000000000..e9a7145ad7b0329c9012db447d5402adc20ec22e --- /dev/null +++ b/auto_run.py @@ -0,0 +1,26 @@ +import subprocess + +def run_script(script_path): + print(f"\n🔹 Đang chạy: {script_path}") + result = subprocess.run(["python", script_path], capture_output=True, text=True) + + if result.returncode == 0: + print(f"✅ Hoàn thành: {script_path}\n") + else: + print(f"❌ Lỗi khi chạy {script_path}:\n{result.stderr}\n") + +if __name__ == "__main__": + # Danh sách file cần chạy tuần tự + scripts = [ + "segmentation/u2net_test.py", + "segmentation/sam_guide.py", + "segmentation/sam_predict.py", + "segmentation/make_final_mask.py", + "alopecia/calculate_hair_thickness.py", + "alopecia/calculate_hair_count.py" + ] + + for script in scripts: + run_script(script) + + print("🎉 Hoàn tất toàn bộ quy trình!") diff --git a/complete_pipeline_out/binary_230219_A191_1.jpg b/complete_pipeline_out/binary_230219_A191_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fe9c608ddb7b0fce5923abd7fc93c00b6d9d84b0 Binary files /dev/null and b/complete_pipeline_out/binary_230219_A191_1.jpg differ diff --git a/complete_pipeline_out/binary_230219_A191_1_test.jpg b/complete_pipeline_out/binary_230219_A191_1_test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fe9c608ddb7b0fce5923abd7fc93c00b6d9d84b0 Binary files /dev/null and b/complete_pipeline_out/binary_230219_A191_1_test.jpg differ diff --git a/complete_pipeline_out/binary_230219_A200_4.jpg b/complete_pipeline_out/binary_230219_A200_4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b3417ab4595673c94983cdb24b0ab173af2f7b01 Binary files /dev/null and b/complete_pipeline_out/binary_230219_A200_4.jpg differ diff --git a/complete_pipeline_out/binary_IMG_02C458E4D7CE-3.jpeg b/complete_pipeline_out/binary_IMG_02C458E4D7CE-3.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..74b762a00faf4961cf7e4e9123083ad654bb7f17 Binary files /dev/null and b/complete_pipeline_out/binary_IMG_02C458E4D7CE-3.jpeg differ diff --git a/complete_pipeline_out/binary_IMG_02C458E4D7CE-4.jpeg b/complete_pipeline_out/binary_IMG_02C458E4D7CE-4.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..5e9ea4e59331a9d847992a6ebc23e104ecaaf0a1 Binary files /dev/null and b/complete_pipeline_out/binary_IMG_02C458E4D7CE-4.jpeg differ diff --git a/complete_pipeline_out/binary_IMG_02C458E4D7CE-5.jpeg b/complete_pipeline_out/binary_IMG_02C458E4D7CE-5.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..9b4f010e7774565c13bc5b4de8c8c7d61104c409 Binary files /dev/null and b/complete_pipeline_out/binary_IMG_02C458E4D7CE-5.jpeg differ diff --git a/complete_pipeline_out/binary_IMG_02C458E4D7CE-6.jpeg b/complete_pipeline_out/binary_IMG_02C458E4D7CE-6.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..909d44187e5727aa63e6a1b03d96a262fdf7be5b Binary files /dev/null and b/complete_pipeline_out/binary_IMG_02C458E4D7CE-6.jpeg differ diff --git a/complete_pipeline_out/complete_pipeline_230219_A191_1.jpg b/complete_pipeline_out/complete_pipeline_230219_A191_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cc9f6aa45bda9973b3220863fa512508556f4fbb --- /dev/null +++ b/complete_pipeline_out/complete_pipeline_230219_A191_1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd5d7ccddf4037a495ed2a2f8ad3ad8fd80f28df29830a29a923cb55b9a5d19e +size 687786 diff --git a/complete_pipeline_out/complete_pipeline_230219_A191_1_test.jpg b/complete_pipeline_out/complete_pipeline_230219_A191_1_test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fd0136e8930c339650b8aa35bbe349ac4e23738f --- /dev/null +++ b/complete_pipeline_out/complete_pipeline_230219_A191_1_test.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:30b012995baab1594be028f86bd32ac4897f3708d642891399db75445f0350a8 +size 688577 diff --git a/complete_pipeline_out/complete_pipeline_230219_A200_4.jpg b/complete_pipeline_out/complete_pipeline_230219_A200_4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a8378af5caf6f066d6253f8d7e2893304451f81a --- /dev/null +++ b/complete_pipeline_out/complete_pipeline_230219_A200_4.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28eed3ad7fda7ca9399c683b8b83bba516dba278f5b735cc36c9472da48b3561 +size 613333 diff --git a/complete_pipeline_out/complete_pipeline_IMG_02C458E4D7CE-3.jpeg b/complete_pipeline_out/complete_pipeline_IMG_02C458E4D7CE-3.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..71d6906af940d77dd1015a1de45145adca8dd8fe --- /dev/null +++ b/complete_pipeline_out/complete_pipeline_IMG_02C458E4D7CE-3.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd2601ee33bd99681015d3e06bc0058ed51b4436498b3d06ee79dcd3360cfb2f +size 920917 diff --git a/complete_pipeline_out/complete_pipeline_IMG_02C458E4D7CE-4.jpeg b/complete_pipeline_out/complete_pipeline_IMG_02C458E4D7CE-4.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..c302dd63cccc6bd9403c3dd35513386b444183e4 --- /dev/null +++ b/complete_pipeline_out/complete_pipeline_IMG_02C458E4D7CE-4.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d91add007553aea64b5960cb4d0e930b751573f662950c78ea438089f09c1377 +size 707485 diff --git a/complete_pipeline_out/complete_pipeline_IMG_02C458E4D7CE-5.jpeg b/complete_pipeline_out/complete_pipeline_IMG_02C458E4D7CE-5.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..a5fa213181d09265daa33edd90c222464f52587d --- /dev/null +++ b/complete_pipeline_out/complete_pipeline_IMG_02C458E4D7CE-5.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07523bc22472468e45590b37f6c3461bfcabc91a8e3fd3f7c3fbc8e1aa40bde1 +size 875048 diff --git a/complete_pipeline_out/complete_pipeline_IMG_02C458E4D7CE-6.jpeg b/complete_pipeline_out/complete_pipeline_IMG_02C458E4D7CE-6.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..56c4805ec2ec6557b5d6b25f3f987c4092514464 --- /dev/null +++ b/complete_pipeline_out/complete_pipeline_IMG_02C458E4D7CE-6.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b7c747c926da2222c9a6d6c5aec9572ebced78aac276f8277c6935ab7e5ea40 +size 894998 diff --git a/complete_pipeline_out/labeled_230219_A191_1.jpg b/complete_pipeline_out/labeled_230219_A191_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7a6c2b0adcefc2db1fd303c88c7d2e685b63c3c6 --- /dev/null +++ b/complete_pipeline_out/labeled_230219_A191_1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:589a8825f19246f6341ddf025013e55f7180e2f5a7dcaae715d85e36a8b4bef5 +size 257894 diff --git a/complete_pipeline_out/labeled_230219_A191_1_test.jpg b/complete_pipeline_out/labeled_230219_A191_1_test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7a6c2b0adcefc2db1fd303c88c7d2e685b63c3c6 --- /dev/null +++ b/complete_pipeline_out/labeled_230219_A191_1_test.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:589a8825f19246f6341ddf025013e55f7180e2f5a7dcaae715d85e36a8b4bef5 +size 257894 diff --git a/complete_pipeline_out/labeled_230219_A200_4.jpg b/complete_pipeline_out/labeled_230219_A200_4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..36dfce844899c8dd2bfd155276a79e717a089b0a --- /dev/null +++ b/complete_pipeline_out/labeled_230219_A200_4.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb35a52b9b3eb296ce9cff174fe98f4e7a9d2f47ff948256599f1f1e5b6210b7 +size 208285 diff --git a/complete_pipeline_out/labeled_IMG_02C458E4D7CE-3.jpeg b/complete_pipeline_out/labeled_IMG_02C458E4D7CE-3.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..d60306c7999d7d8ee568f0f7540580c715a37cb9 --- /dev/null +++ b/complete_pipeline_out/labeled_IMG_02C458E4D7CE-3.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:073f1b183bf72f0faedc253149ec6e6862178441a7e075bc9d28c3d4f1fbab34 +size 389760 diff --git a/complete_pipeline_out/labeled_IMG_02C458E4D7CE-4.jpeg b/complete_pipeline_out/labeled_IMG_02C458E4D7CE-4.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..0ce35e035f2360eff92db141e468c858c8320a53 --- /dev/null +++ b/complete_pipeline_out/labeled_IMG_02C458E4D7CE-4.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6c17d9c98b9306b3da8258ce55bef5463a8ba1df298daf6dfedf1db9dbf749e +size 299665 diff --git a/complete_pipeline_out/labeled_IMG_02C458E4D7CE-5.jpeg b/complete_pipeline_out/labeled_IMG_02C458E4D7CE-5.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..39115fcc2d160073b2bd6a81097c0a6a59d2a7c0 --- /dev/null +++ b/complete_pipeline_out/labeled_IMG_02C458E4D7CE-5.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f0e6dac61f915e18f76d76c651fe48c83a774435cdcc5c382cd6b35c778d871 +size 359685 diff --git a/complete_pipeline_out/labeled_IMG_02C458E4D7CE-6.jpeg b/complete_pipeline_out/labeled_IMG_02C458E4D7CE-6.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..5209bb4371090d0e46c7b93c2af730099d196659 --- /dev/null +++ b/complete_pipeline_out/labeled_IMG_02C458E4D7CE-6.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:145738aad11eb0b4ab4f8409114f210207331b206490c927192b49d38b4eebe9 +size 373478 diff --git a/complete_pipeline_out/lines_mask_230219_A191_1.jpg b/complete_pipeline_out/lines_mask_230219_A191_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..23549c0146251295f51dcb85f50b41a68be7585d Binary files /dev/null and b/complete_pipeline_out/lines_mask_230219_A191_1.jpg differ diff --git a/complete_pipeline_out/lines_mask_230219_A191_1_test.jpg b/complete_pipeline_out/lines_mask_230219_A191_1_test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..23549c0146251295f51dcb85f50b41a68be7585d Binary files /dev/null and b/complete_pipeline_out/lines_mask_230219_A191_1_test.jpg differ diff --git a/complete_pipeline_out/lines_mask_230219_A200_4.jpg b/complete_pipeline_out/lines_mask_230219_A200_4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8a15e7954ef703e60cf379ad2dc6cad8f9344ab1 Binary files /dev/null and b/complete_pipeline_out/lines_mask_230219_A200_4.jpg differ diff --git a/complete_pipeline_out/lines_mask_IMG_02C458E4D7CE-3.jpeg b/complete_pipeline_out/lines_mask_IMG_02C458E4D7CE-3.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..13cce70dccd9915588ce62b78c448f46da714c15 --- /dev/null +++ b/complete_pipeline_out/lines_mask_IMG_02C458E4D7CE-3.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86e0bae7bb59d540a22c291984e757f5a7ab5ee2f34cec0ff6bcf4fcc81e002f +size 155060 diff --git a/complete_pipeline_out/lines_mask_IMG_02C458E4D7CE-4.jpeg b/complete_pipeline_out/lines_mask_IMG_02C458E4D7CE-4.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..055f381d3492877fae5e6c7f072b68d831fcdec6 Binary files /dev/null and b/complete_pipeline_out/lines_mask_IMG_02C458E4D7CE-4.jpeg differ diff --git a/complete_pipeline_out/lines_mask_IMG_02C458E4D7CE-5.jpeg b/complete_pipeline_out/lines_mask_IMG_02C458E4D7CE-5.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..8635d24d4abdd8450813fa98ac9d201b188b672e --- /dev/null +++ b/complete_pipeline_out/lines_mask_IMG_02C458E4D7CE-5.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c0155fa9ce2e19313c6c2884f27036ecb23b990b059c2d088f4e166f5110fdb +size 122031 diff --git a/complete_pipeline_out/lines_mask_IMG_02C458E4D7CE-6.jpeg b/complete_pipeline_out/lines_mask_IMG_02C458E4D7CE-6.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..5726d78994f48a401af4c0abc46d8c56222a9c59 --- /dev/null +++ b/complete_pipeline_out/lines_mask_IMG_02C458E4D7CE-6.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e480e5a693d91f960d41d049228d07a3ce7b84bf45f26f56613912da827f477 +size 159374 diff --git a/complete_pipeline_out/merged_foreground_230219_A191_1.jpg b/complete_pipeline_out/merged_foreground_230219_A191_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..44738293383527bcb498946ced8f1946c40bac2c Binary files /dev/null and b/complete_pipeline_out/merged_foreground_230219_A191_1.jpg differ diff --git a/complete_pipeline_out/merged_foreground_230219_A191_1_test.jpg b/complete_pipeline_out/merged_foreground_230219_A191_1_test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..44738293383527bcb498946ced8f1946c40bac2c Binary files /dev/null and b/complete_pipeline_out/merged_foreground_230219_A191_1_test.jpg differ diff --git a/complete_pipeline_out/merged_foreground_230219_A200_4.jpg b/complete_pipeline_out/merged_foreground_230219_A200_4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8bdf49c83708c5830b12c20dd044d14b9d15737c Binary files /dev/null and b/complete_pipeline_out/merged_foreground_230219_A200_4.jpg differ diff --git a/complete_pipeline_out/merged_foreground_IMG_02C458E4D7CE-3.jpeg b/complete_pipeline_out/merged_foreground_IMG_02C458E4D7CE-3.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..41e311b6a753f6bf16fefdbb3206aef2378469ba --- /dev/null +++ b/complete_pipeline_out/merged_foreground_IMG_02C458E4D7CE-3.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af2c0ee0440a9643d3dd5df6d85eeb83fcaa9aa25f5c2e4457fc2eece16bd0dc +size 107643 diff --git a/complete_pipeline_out/merged_foreground_IMG_02C458E4D7CE-4.jpeg b/complete_pipeline_out/merged_foreground_IMG_02C458E4D7CE-4.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..9ddd338073c14e18b4416bf2411aa66b6520deef Binary files /dev/null and b/complete_pipeline_out/merged_foreground_IMG_02C458E4D7CE-4.jpeg differ diff --git a/complete_pipeline_out/merged_foreground_IMG_02C458E4D7CE-5.jpeg b/complete_pipeline_out/merged_foreground_IMG_02C458E4D7CE-5.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..61cd617e124663c68fb977aa18cf7a691bc0b31a Binary files /dev/null and b/complete_pipeline_out/merged_foreground_IMG_02C458E4D7CE-5.jpeg differ diff --git a/complete_pipeline_out/merged_foreground_IMG_02C458E4D7CE-6.jpeg b/complete_pipeline_out/merged_foreground_IMG_02C458E4D7CE-6.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..3d4f71651ff6c8556abdc4708dbd31476b2f86b9 --- /dev/null +++ b/complete_pipeline_out/merged_foreground_IMG_02C458E4D7CE-6.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8ddcab8169e6dc8449d58038afe5b9060a38d491e8949760f84d7c9e4cf8e9d +size 116773 diff --git a/datasets/data/230219_A191_1.jpg b/datasets/data/230219_A191_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2912e04505d45302121b500d06692f78cdff8fc0 --- /dev/null +++ b/datasets/data/230219_A191_1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db245e9ea2a52a5c0fc044f06e16128019e69d2ee34c6f1e9f01cba268a75363 +size 115816 diff --git a/datasets/data/230219_A191_1_test.jpg b/datasets/data/230219_A191_1_test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2912e04505d45302121b500d06692f78cdff8fc0 --- /dev/null +++ b/datasets/data/230219_A191_1_test.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db245e9ea2a52a5c0fc044f06e16128019e69d2ee34c6f1e9f01cba268a75363 +size 115816 diff --git a/datasets/data/230219_A200_4.jpg b/datasets/data/230219_A200_4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f25d8e1821ec0d08324a7a0fe57bfe4acddb21a6 Binary files /dev/null and b/datasets/data/230219_A200_4.jpg differ diff --git a/datasets/data/IMG_02C458E4D7CE-3.jpeg b/datasets/data/IMG_02C458E4D7CE-3.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..7cdd13f257f58919b7bbc159156b8afa978c605a --- /dev/null +++ b/datasets/data/IMG_02C458E4D7CE-3.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60b67ff873b0cbd5369392082f1803fbb177d97b8d7293929141ffc127479ead +size 954870 diff --git a/datasets/data/IMG_02C458E4D7CE-4.jpeg b/datasets/data/IMG_02C458E4D7CE-4.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..49818d85b1eda91fa3e61ff509a5eb65491e78e8 --- /dev/null +++ b/datasets/data/IMG_02C458E4D7CE-4.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b1f932b91051382d478129a1f62069c7abcfa41e7c4956e533fab0fa09b4e55 +size 826050 diff --git a/datasets/data/IMG_02C458E4D7CE-5.jpeg b/datasets/data/IMG_02C458E4D7CE-5.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..40f8d19c6a60f5c5bbf439c86ac2f91004c44e4e --- /dev/null +++ b/datasets/data/IMG_02C458E4D7CE-5.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa88888b08601d28f61a7ddeba92e8065685787fefbf67b68e54f6683f97037e +size 933550 diff --git a/datasets/data/IMG_02C458E4D7CE-6.jpeg b/datasets/data/IMG_02C458E4D7CE-6.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..3768d7a667ca509f8575d26b60b9fe1f4e68bed2 --- /dev/null +++ b/datasets/data/IMG_02C458E4D7CE-6.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4605ce413118f4e4335aef7713bc40adefc7eec6a4cfc151a20f884ae2de67bb +size 927707 diff --git a/datasets/data/IMG_02C458E4D7CE-7.jpeg b/datasets/data/IMG_02C458E4D7CE-7.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..04e7a91db271e42e55302e16ecf1fd1e5ff2501c --- /dev/null +++ b/datasets/data/IMG_02C458E4D7CE-7.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:adad05898261c3a59a0e700b659ffe251ec46194baa54566a818457b2cbf7105 +size 1008571 diff --git a/datasets/data/IMG_02C458E4D7CE-9.jpeg b/datasets/data/IMG_02C458E4D7CE-9.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..d1316e2a60ecbb0d17764805d05e38660e78b76c --- /dev/null +++ b/datasets/data/IMG_02C458E4D7CE-9.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:165708e62d620f93ae766bf7f0ee77e34d9c7cf4bce1f31dba9996d440ed3e6c +size 938887 diff --git a/datasets/output/Result_230219_A191_1.png b/datasets/output/Result_230219_A191_1.png new file mode 100644 index 0000000000000000000000000000000000000000..763d9a11c8335bfa12a04ae7dca5ecf505446227 Binary files /dev/null and b/datasets/output/Result_230219_A191_1.png differ diff --git a/datasets/output/Result_230219_A191_1_test.png b/datasets/output/Result_230219_A191_1_test.png new file mode 100644 index 0000000000000000000000000000000000000000..763d9a11c8335bfa12a04ae7dca5ecf505446227 Binary files /dev/null and b/datasets/output/Result_230219_A191_1_test.png differ diff --git a/datasets/output/Result_230219_A200_4.png b/datasets/output/Result_230219_A200_4.png new file mode 100644 index 0000000000000000000000000000000000000000..6c80ee204d96730fde19497990394622484377ed Binary files /dev/null and b/datasets/output/Result_230219_A200_4.png differ diff --git a/datasets/output/Result_IMG_02C458E4D7CE-3.png b/datasets/output/Result_IMG_02C458E4D7CE-3.png new file mode 100644 index 0000000000000000000000000000000000000000..7ac8ad21ae837d99c634f204bb516f594a45d77f Binary files /dev/null and b/datasets/output/Result_IMG_02C458E4D7CE-3.png differ diff --git a/datasets/output/Result_IMG_02C458E4D7CE-4.png b/datasets/output/Result_IMG_02C458E4D7CE-4.png new file mode 100644 index 0000000000000000000000000000000000000000..9b221d01102c0d79a2451f245537b6856f53caca Binary files /dev/null and b/datasets/output/Result_IMG_02C458E4D7CE-4.png differ diff --git a/datasets/output/Result_IMG_02C458E4D7CE-5.png b/datasets/output/Result_IMG_02C458E4D7CE-5.png new file mode 100644 index 0000000000000000000000000000000000000000..82145a840dce54fda24db1f7618a1cf74e6b76ef Binary files /dev/null and b/datasets/output/Result_IMG_02C458E4D7CE-5.png differ diff --git a/datasets/output/Result_IMG_02C458E4D7CE-6.png b/datasets/output/Result_IMG_02C458E4D7CE-6.png new file mode 100644 index 0000000000000000000000000000000000000000..2844b37ecb52a25a9d6fcbb82feed0d7023338ca Binary files /dev/null and b/datasets/output/Result_IMG_02C458E4D7CE-6.png differ diff --git a/datasets/output/Result_IMG_02C458E4D7CE-7.png b/datasets/output/Result_IMG_02C458E4D7CE-7.png new file mode 100644 index 0000000000000000000000000000000000000000..3f4f6405c4e80c5ebcab390d38e4e453a1524d6c Binary files /dev/null and b/datasets/output/Result_IMG_02C458E4D7CE-7.png differ diff --git a/datasets/output/Result_IMG_02C458E4D7CE-9.png b/datasets/output/Result_IMG_02C458E4D7CE-9.png new file mode 100644 index 0000000000000000000000000000000000000000..2c353ae9e0bd5aee1477fb28423fc7aabdc70410 Binary files /dev/null and b/datasets/output/Result_IMG_02C458E4D7CE-9.png differ diff --git a/datasets/output/Result_IMG_09C45E7564E7-1.png b/datasets/output/Result_IMG_09C45E7564E7-1.png new file mode 100644 index 0000000000000000000000000000000000000000..32f3d7f942df2d4db8e722bf9b251a790ae9c7f2 Binary files /dev/null and b/datasets/output/Result_IMG_09C45E7564E7-1.png differ diff --git a/datasets/output/Result_IMG_09C45E7564E7-2.png b/datasets/output/Result_IMG_09C45E7564E7-2.png new file mode 100644 index 0000000000000000000000000000000000000000..8f94b776473110370b64fb66e1d675d4d996b88e Binary files /dev/null and b/datasets/output/Result_IMG_09C45E7564E7-2.png differ diff --git a/datasets/output/Result_IMG_09C45E7564E7-3.png b/datasets/output/Result_IMG_09C45E7564E7-3.png new file mode 100644 index 0000000000000000000000000000000000000000..5f5af4d1cb70017823683c85147baf5cd13fbe17 Binary files /dev/null and b/datasets/output/Result_IMG_09C45E7564E7-3.png differ diff --git a/datasets/output/Result_IMG_09C45E7564E7-4.png b/datasets/output/Result_IMG_09C45E7564E7-4.png new file mode 100644 index 0000000000000000000000000000000000000000..55aa5f4e75c122ea094470710a51925e76937e18 Binary files /dev/null and b/datasets/output/Result_IMG_09C45E7564E7-4.png differ diff --git a/datasets/output/Result_IMG_09C45E7564E7-5.png b/datasets/output/Result_IMG_09C45E7564E7-5.png new file mode 100644 index 0000000000000000000000000000000000000000..b22be6d129ac3f1e1eec0a2ec23e5f47025ad8c2 Binary files /dev/null and b/datasets/output/Result_IMG_09C45E7564E7-5.png differ diff --git a/datasets/output/Result_IMG_09C45E7564E7-6.png b/datasets/output/Result_IMG_09C45E7564E7-6.png new file mode 100644 index 0000000000000000000000000000000000000000..f9ef2a48e4f6fb7cfb8b4dfb7a87600733c4560c Binary files /dev/null and b/datasets/output/Result_IMG_09C45E7564E7-6.png differ diff --git a/datasets/output/SkeletonContour_230219_A191_1.png b/datasets/output/SkeletonContour_230219_A191_1.png new file mode 100644 index 0000000000000000000000000000000000000000..e37acee430ba7478608cd98e481146ab63c332d3 Binary files /dev/null and b/datasets/output/SkeletonContour_230219_A191_1.png differ diff --git a/datasets/output/SkeletonContour_230219_A191_1_test.png b/datasets/output/SkeletonContour_230219_A191_1_test.png new file mode 100644 index 0000000000000000000000000000000000000000..e37acee430ba7478608cd98e481146ab63c332d3 Binary files /dev/null and b/datasets/output/SkeletonContour_230219_A191_1_test.png differ diff --git a/datasets/output/SkeletonContour_230219_A200_4.png b/datasets/output/SkeletonContour_230219_A200_4.png new file mode 100644 index 0000000000000000000000000000000000000000..6f61c6546c2c3f4858b08f7111055d283b25382d Binary files /dev/null and b/datasets/output/SkeletonContour_230219_A200_4.png differ diff --git a/datasets/output/SkeletonContour_IMG_02C458E4D7CE-3.png b/datasets/output/SkeletonContour_IMG_02C458E4D7CE-3.png new file mode 100644 index 0000000000000000000000000000000000000000..53b7fa477c7b4e90980fc2afb6a9f71002c24a13 Binary files /dev/null and b/datasets/output/SkeletonContour_IMG_02C458E4D7CE-3.png differ diff --git a/datasets/output/SkeletonContour_IMG_02C458E4D7CE-4.png b/datasets/output/SkeletonContour_IMG_02C458E4D7CE-4.png new file mode 100644 index 0000000000000000000000000000000000000000..7a8250d84203b3d2629042da5dfb90e814a24905 Binary files /dev/null and b/datasets/output/SkeletonContour_IMG_02C458E4D7CE-4.png differ diff --git a/datasets/output/SkeletonContour_IMG_02C458E4D7CE-5.png b/datasets/output/SkeletonContour_IMG_02C458E4D7CE-5.png new file mode 100644 index 0000000000000000000000000000000000000000..071931dbf544130e2678d24fd79ceb4b5dab8dd8 Binary files /dev/null and b/datasets/output/SkeletonContour_IMG_02C458E4D7CE-5.png differ diff --git a/datasets/output/SkeletonContour_IMG_02C458E4D7CE-6.png b/datasets/output/SkeletonContour_IMG_02C458E4D7CE-6.png new file mode 100644 index 0000000000000000000000000000000000000000..f25494fdcbff65a5eff007f52c905f986cf8d701 Binary files /dev/null and b/datasets/output/SkeletonContour_IMG_02C458E4D7CE-6.png differ diff --git a/datasets/output/SkeletonContour_IMG_02C458E4D7CE-7.png b/datasets/output/SkeletonContour_IMG_02C458E4D7CE-7.png new file mode 100644 index 0000000000000000000000000000000000000000..a0bf046420ef7e0b5be63d34a03cee20a6f18ee4 Binary files /dev/null and b/datasets/output/SkeletonContour_IMG_02C458E4D7CE-7.png differ diff --git a/datasets/output/SkeletonContour_IMG_02C458E4D7CE-9.png b/datasets/output/SkeletonContour_IMG_02C458E4D7CE-9.png new file mode 100644 index 0000000000000000000000000000000000000000..dd46a23d28048f5f35e9c12fb0c765460ed7befd Binary files /dev/null and b/datasets/output/SkeletonContour_IMG_02C458E4D7CE-9.png differ diff --git a/datasets/output/SkeletonContour_IMG_09C45E7564E7-1.png b/datasets/output/SkeletonContour_IMG_09C45E7564E7-1.png new file mode 100644 index 0000000000000000000000000000000000000000..39059292a4f90a81f853be5a21f6badc704604d2 Binary files /dev/null and b/datasets/output/SkeletonContour_IMG_09C45E7564E7-1.png differ diff --git a/datasets/output/SkeletonContour_IMG_09C45E7564E7-2.png b/datasets/output/SkeletonContour_IMG_09C45E7564E7-2.png new file mode 100644 index 0000000000000000000000000000000000000000..a84b1494c71775104eb88b1b4430d10a7ea42a0e Binary files /dev/null and b/datasets/output/SkeletonContour_IMG_09C45E7564E7-2.png differ diff --git a/datasets/output/SkeletonContour_IMG_09C45E7564E7-3.png b/datasets/output/SkeletonContour_IMG_09C45E7564E7-3.png new file mode 100644 index 0000000000000000000000000000000000000000..4988fbeadc65e636886e94ae0e13fe21f78fd8c4 Binary files /dev/null and b/datasets/output/SkeletonContour_IMG_09C45E7564E7-3.png differ diff --git a/datasets/output/SkeletonContour_IMG_09C45E7564E7-4.png b/datasets/output/SkeletonContour_IMG_09C45E7564E7-4.png new file mode 100644 index 0000000000000000000000000000000000000000..5e837300030cdf703c559093dafdbf185673aae9 Binary files /dev/null and b/datasets/output/SkeletonContour_IMG_09C45E7564E7-4.png differ diff --git a/datasets/output/SkeletonContour_IMG_09C45E7564E7-5.png b/datasets/output/SkeletonContour_IMG_09C45E7564E7-5.png new file mode 100644 index 0000000000000000000000000000000000000000..27e1057339585cf0f88822ab9020a6bbb4605901 Binary files /dev/null and b/datasets/output/SkeletonContour_IMG_09C45E7564E7-5.png differ diff --git a/datasets/output/SkeletonContour_IMG_09C45E7564E7-6.png b/datasets/output/SkeletonContour_IMG_09C45E7564E7-6.png new file mode 100644 index 0000000000000000000000000000000000000000..49c848e70342cf4cc51593a8a52b067ffdfb1471 Binary files /dev/null and b/datasets/output/SkeletonContour_IMG_09C45E7564E7-6.png differ diff --git a/datasets/output/Skeleton_230219_A191_1.png b/datasets/output/Skeleton_230219_A191_1.png new file mode 100644 index 0000000000000000000000000000000000000000..d9628e31a8b3071de28386c15052d9e8244cd40a Binary files /dev/null and b/datasets/output/Skeleton_230219_A191_1.png differ diff --git a/datasets/output/Skeleton_230219_A191_1_test.png b/datasets/output/Skeleton_230219_A191_1_test.png new file mode 100644 index 0000000000000000000000000000000000000000..d9628e31a8b3071de28386c15052d9e8244cd40a Binary files /dev/null and b/datasets/output/Skeleton_230219_A191_1_test.png differ diff --git a/datasets/output/Skeleton_230219_A200_4.png b/datasets/output/Skeleton_230219_A200_4.png new file mode 100644 index 0000000000000000000000000000000000000000..2deb171929708106517eded72cbb76bfdf0998b9 Binary files /dev/null and b/datasets/output/Skeleton_230219_A200_4.png differ diff --git a/datasets/output/Skeleton_IMG_02C458E4D7CE-3.png b/datasets/output/Skeleton_IMG_02C458E4D7CE-3.png new file mode 100644 index 0000000000000000000000000000000000000000..0973d93bac4680ffbd7200d44edc13fe4c6476c5 Binary files /dev/null and b/datasets/output/Skeleton_IMG_02C458E4D7CE-3.png differ diff --git a/datasets/output/Skeleton_IMG_02C458E4D7CE-4.png b/datasets/output/Skeleton_IMG_02C458E4D7CE-4.png new file mode 100644 index 0000000000000000000000000000000000000000..1fa99de9ffc346a62f624fd0ccc7c00e3ff6b188 Binary files /dev/null and b/datasets/output/Skeleton_IMG_02C458E4D7CE-4.png differ diff --git a/datasets/output/Skeleton_IMG_02C458E4D7CE-5.png b/datasets/output/Skeleton_IMG_02C458E4D7CE-5.png new file mode 100644 index 0000000000000000000000000000000000000000..284c3b56f1978d0c072b354034b831e7861be8bf Binary files /dev/null and b/datasets/output/Skeleton_IMG_02C458E4D7CE-5.png differ diff --git a/datasets/output/Skeleton_IMG_02C458E4D7CE-6.png b/datasets/output/Skeleton_IMG_02C458E4D7CE-6.png new file mode 100644 index 0000000000000000000000000000000000000000..1210048c05993fad29486626e6bf427c78ab4b9e Binary files /dev/null and b/datasets/output/Skeleton_IMG_02C458E4D7CE-6.png differ diff --git a/datasets/output/Skeleton_IMG_02C458E4D7CE-7.png b/datasets/output/Skeleton_IMG_02C458E4D7CE-7.png new file mode 100644 index 0000000000000000000000000000000000000000..6c5db2b663308167ead2ab522f4069d25aa955b8 Binary files /dev/null and b/datasets/output/Skeleton_IMG_02C458E4D7CE-7.png differ diff --git a/datasets/output/Skeleton_IMG_02C458E4D7CE-9.png b/datasets/output/Skeleton_IMG_02C458E4D7CE-9.png new file mode 100644 index 0000000000000000000000000000000000000000..9e09e9d3c4ed6d9112090f26ac0115e54576b507 Binary files /dev/null and b/datasets/output/Skeleton_IMG_02C458E4D7CE-9.png differ diff --git a/datasets/output/Skeleton_IMG_09C45E7564E7-1.png b/datasets/output/Skeleton_IMG_09C45E7564E7-1.png new file mode 100644 index 0000000000000000000000000000000000000000..f84415f96c917d0f01b13767ae2844a8351847e5 Binary files /dev/null and b/datasets/output/Skeleton_IMG_09C45E7564E7-1.png differ diff --git a/datasets/output/Skeleton_IMG_09C45E7564E7-2.png b/datasets/output/Skeleton_IMG_09C45E7564E7-2.png new file mode 100644 index 0000000000000000000000000000000000000000..fdf78dd4ea2f526050c0ad56fcb53b5679d48ab6 Binary files /dev/null and b/datasets/output/Skeleton_IMG_09C45E7564E7-2.png differ diff --git a/datasets/output/Skeleton_IMG_09C45E7564E7-3.png b/datasets/output/Skeleton_IMG_09C45E7564E7-3.png new file mode 100644 index 0000000000000000000000000000000000000000..df63fe6aef08dd2f77f32f0320c0bc6ff95e43f4 Binary files /dev/null and b/datasets/output/Skeleton_IMG_09C45E7564E7-3.png differ diff --git a/datasets/output/Skeleton_IMG_09C45E7564E7-4.png b/datasets/output/Skeleton_IMG_09C45E7564E7-4.png new file mode 100644 index 0000000000000000000000000000000000000000..69110f0143cecb0e8c088e6e109f5acdad04ea1e Binary files /dev/null and b/datasets/output/Skeleton_IMG_09C45E7564E7-4.png differ diff --git a/datasets/output/Skeleton_IMG_09C45E7564E7-5.png b/datasets/output/Skeleton_IMG_09C45E7564E7-5.png new file mode 100644 index 0000000000000000000000000000000000000000..69f26ec541f361b6c04c6dc99d5579de7f01c808 Binary files /dev/null and b/datasets/output/Skeleton_IMG_09C45E7564E7-5.png differ diff --git a/datasets/output/Skeleton_IMG_09C45E7564E7-6.png b/datasets/output/Skeleton_IMG_09C45E7564E7-6.png new file mode 100644 index 0000000000000000000000000000000000000000..d96b1565c0c25ecc1bf61066dd5ab850c7189e23 Binary files /dev/null and b/datasets/output/Skeleton_IMG_09C45E7564E7-6.png differ diff --git a/datasets/seg_train/230219_A191_1.jpg b/datasets/seg_train/230219_A191_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c3c5646ba77ad888a8ed8849adbc0de2a442d370 Binary files /dev/null and b/datasets/seg_train/230219_A191_1.jpg differ diff --git a/datasets/seg_train/230219_A191_1_test.jpg b/datasets/seg_train/230219_A191_1_test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c3c5646ba77ad888a8ed8849adbc0de2a442d370 Binary files /dev/null and b/datasets/seg_train/230219_A191_1_test.jpg differ diff --git a/datasets/seg_train/230219_A200_4.jpg b/datasets/seg_train/230219_A200_4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5a0b9f70eb590067afad8475fa5ee94f9becdb70 Binary files /dev/null and b/datasets/seg_train/230219_A200_4.jpg differ diff --git a/datasets/seg_train/IMG_02C458E4D7CE-3.jpg b/datasets/seg_train/IMG_02C458E4D7CE-3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5686defe0bc2bf9917f59279791e50ea09001cbd Binary files /dev/null and b/datasets/seg_train/IMG_02C458E4D7CE-3.jpg differ diff --git a/datasets/seg_train/IMG_02C458E4D7CE-4.jpg b/datasets/seg_train/IMG_02C458E4D7CE-4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5fe517c9840357d7334ab1834ae275101ce546e2 Binary files /dev/null and b/datasets/seg_train/IMG_02C458E4D7CE-4.jpg differ diff --git a/datasets/seg_train/IMG_02C458E4D7CE-5.jpg b/datasets/seg_train/IMG_02C458E4D7CE-5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dad68d47d7f2f50cee39fba89c2fc1ae69a3bf9d Binary files /dev/null and b/datasets/seg_train/IMG_02C458E4D7CE-5.jpg differ diff --git a/datasets/seg_train/IMG_02C458E4D7CE-6.jpg b/datasets/seg_train/IMG_02C458E4D7CE-6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ab6662eba513b7657b196758ea111b9643e9be2b Binary files /dev/null and b/datasets/seg_train/IMG_02C458E4D7CE-6.jpg differ diff --git a/datasets/seg_train/IMG_02C458E4D7CE-7.jpg b/datasets/seg_train/IMG_02C458E4D7CE-7.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2ac7520ac29caf7d63a84289d15a40038f80d9af Binary files /dev/null and b/datasets/seg_train/IMG_02C458E4D7CE-7.jpg differ diff --git a/datasets/seg_train/IMG_02C458E4D7CE-9.jpg b/datasets/seg_train/IMG_02C458E4D7CE-9.jpg new file mode 100644 index 0000000000000000000000000000000000000000..155b40a092532a69919982f9cc8e5d4cbb7609b0 Binary files /dev/null and b/datasets/seg_train/IMG_02C458E4D7CE-9.jpg differ diff --git a/datasets/train_bbox_points.json b/datasets/train_bbox_points.json new file mode 100644 index 0000000000000000000000000000000000000000..6d61fb60d9537bf699bdf42998cd77e31a179821 --- /dev/null +++ b/datasets/train_bbox_points.json @@ -0,0 +1 @@ +{"230219_A191_1.jpg": [[124, 412, 134, 422], [119, 403, 129, 413], [147, 396, 157, 406], [120, 392, 130, 402], [138, 390, 148, 400], [123, 382, 133, 392], [117, 372, 127, 382], [113, 362, 123, 372], [108, 353, 118, 363], [103, 344, 113, 354], [98, 334, 108, 344], [93, 325, 103, 335], [85, 318, 95, 328], [78, 309, 88, 319], [71, 300, 81, 310], [64, 291, 74, 301], [557, 251, 567, 261], [550, 243, 560, 253], [545, 234, 555, 244], [540, 225, 550, 235], [536, 215, 546, 225], [530, 206, 540, 216], [525, 197, 535, 207], [520, 188, 530, 198], [515, 179, 525, 189], [510, 170, 520, 180], [505, 160, 515, 170], [500, 151, 510, 161], [493, 142, 503, 152], [487, 133, 497, 143], [480, 125, 490, 135], [474, 116, 484, 126], [467, 107, 477, 117], [461, 98, 471, 108], [456, 89, 466, 99], [446, 89, 456, 99], [437, 83, 447, 93], [428, 77, 438, 87], [459, 75, 469, 85], [419, 70, 429, 80], [458, 65, 468, 75], [411, 63, 421, 73], [457, 55, 467, 65], [404, 54, 414, 64], [455, 45, 465, 55], [399, 45, 409, 55], [385, 6, 395, 16], [407, 4, 417, 14], [396, 3, 406, 13]], "230219_A191_1_test.jpg": [[124, 412, 134, 422], [119, 403, 129, 413], [147, 396, 157, 406], [120, 392, 130, 402], [138, 390, 148, 400], [123, 382, 133, 392], [117, 372, 127, 382], [113, 362, 123, 372], [108, 353, 118, 363], [103, 344, 113, 354], [98, 334, 108, 344], [93, 325, 103, 335], [85, 318, 95, 328], [78, 309, 88, 319], [71, 300, 81, 310], [64, 291, 74, 301], [557, 251, 567, 261], [550, 243, 560, 253], [545, 234, 555, 244], [540, 225, 550, 235], [536, 215, 546, 225], [530, 206, 540, 216], [525, 197, 535, 207], [520, 188, 530, 198], [515, 179, 525, 189], [510, 170, 520, 180], [505, 160, 515, 170], [500, 151, 510, 161], [493, 142, 503, 152], [487, 133, 497, 143], [480, 125, 490, 135], [474, 116, 484, 126], [467, 107, 477, 117], [461, 98, 471, 108], [456, 89, 466, 99], [446, 89, 456, 99], [437, 83, 447, 93], [428, 77, 438, 87], [459, 75, 469, 85], [419, 70, 429, 80], [458, 65, 468, 75], [411, 63, 421, 73], [457, 55, 467, 65], [404, 54, 414, 64], [455, 45, 465, 55], [399, 45, 409, 55], [385, 6, 395, 16], [407, 4, 417, 14], [396, 3, 406, 13]], "230219_A200_4.jpg": [[618, 411, 628, 421], [589, 405, 599, 415], [610, 404, 620, 414], [293, 401, 303, 411], [603, 396, 613, 406], [293, 389, 303, 399], [595, 388, 605, 398], [589, 379, 599, 389], [293, 376, 303, 386], [582, 370, 592, 380], [290, 366, 300, 376], [575, 361, 585, 371], [283, 357, 293, 367], [570, 352, 580, 362], [277, 348, 287, 358], [563, 344, 573, 354], [272, 339, 282, 349], [556, 336, 566, 346], [265, 330, 275, 340], [549, 328, 559, 338], [260, 321, 270, 331], [543, 319, 553, 329], [253, 312, 263, 322], [537, 310, 547, 320], [248, 303, 258, 313], [530, 301, 540, 311], [242, 294, 252, 304], [523, 293, 533, 303], [516, 285, 526, 295], [236, 285, 246, 295], [510, 276, 520, 286], [229, 276, 239, 286], [297, 275, 307, 285], [285, 269, 295, 279], [224, 267, 234, 277], [504, 267, 514, 277], [277, 262, 287, 272], [217, 259, 227, 269], [498, 258, 508, 268], [269, 255, 279, 265], [212, 249, 222, 259], [495, 248, 505, 258], [260, 247, 270, 257], [205, 241, 215, 251], [252, 240, 262, 250], [487, 240, 497, 250], [243, 234, 253, 244], [200, 232, 210, 242], [234, 227, 244, 237], [208, 224, 218, 234], [194, 223, 204, 233], [225, 221, 235, 231], [188, 214, 198, 224], [214, 211, 224, 221], [195, 205, 205, 215], [183, 204, 193, 214], [176, 195, 186, 205], [170, 186, 180, 196], [160, 184, 170, 194], [152, 176, 162, 186], [172, 175, 182, 185], [148, 166, 158, 176], [169, 165, 179, 175], [576, 44, 586, 54], [592, 40, 602, 50], [583, 35, 593, 45], [576, 27, 586, 37], [189, 25, 199, 35], [569, 19, 579, 29], [180, 18, 190, 28], [562, 10, 572, 20], [175, 9, 185, 19], [555, 2, 565, 12], [168, 1, 178, 11]], "IMG_02C458E4D7CE-3.jpg": [[415, 474, 425, 484], [4, 467, 14, 477], [424, 466, 434, 476], [435, 465, 445, 475], [13, 462, 23, 472], [22, 456, 32, 466], [433, 455, 443, 465], [445, 452, 455, 462], [32, 450, 42, 460], [14, 448, 24, 458], [41, 444, 51, 454], [5, 443, 15, 453], [49, 437, 59, 447], [14, 437, 24, 447], [36, 434, 46, 444], [23, 432, 33, 442], [58, 432, 68, 442], [74, 431, 84, 441], [44, 427, 54, 437], [89, 420, 99, 430], [1, 419, 11, 429], [48, 417, 58, 427], [65, 417, 75, 427], [100, 414, 110, 424], [11, 412, 21, 422], [74, 411, 84, 421], [19, 405, 29, 415], [83, 404, 93, 414], [9, 401, 19, 411], [97, 400, 107, 410], [75, 394, 85, 404], [105, 393, 115, 403], [120, 387, 130, 397], [80, 385, 90, 395], [9, 384, 19, 394], [25, 384, 35, 394], [111, 378, 121, 388], [34, 377, 44, 387], [44, 372, 54, 382], [71, 367, 81, 377], [53, 365, 63, 375], [62, 358, 72, 368], [8, 355, 18, 365], [70, 350, 80, 360], [34, 349, 44, 359], [22, 348, 32, 358], [1, 346, 11, 356], [78, 343, 88, 353], [11, 341, 21, 351], [41, 339, 51, 349], [56, 336, 66, 346], [87, 336, 97, 346], [20, 334, 30, 344], [96, 328, 106, 338], [109, 325, 119, 335], [79, 324, 89, 334], [25, 321, 35, 331], [3, 321, 13, 331], [116, 316, 126, 326], [91, 315, 101, 325], [34, 315, 44, 325], [67, 314, 77, 324], [54, 312, 64, 322], [43, 310, 53, 320], [124, 309, 134, 319], [76, 307, 86, 317], [21, 306, 31, 316], [137, 306, 147, 316], [86, 303, 96, 313], [57, 302, 67, 312], [95, 298, 105, 308], [122, 298, 132, 308], [76, 297, 86, 307], [143, 296, 153, 306], [64, 294, 74, 304], [104, 293, 114, 303], [44, 290, 54, 300], [152, 290, 162, 300], [114, 289, 124, 299], [71, 286, 81, 296], [34, 286, 44, 296], [59, 284, 69, 294], [161, 284, 171, 294], [122, 282, 132, 292], [151, 279, 161, 289], [132, 278, 142, 288], [170, 278, 180, 288], [194, 276, 204, 286], [54, 275, 64, 285], [141, 273, 151, 283], [162, 271, 172, 281], [178, 271, 188, 281], [151, 268, 161, 278], [189, 267, 199, 277], [55, 265, 65, 275], [198, 261, 208, 271], [159, 261, 169, 271], [176, 260, 186, 270], [222, 259, 232, 269], [185, 255, 195, 265], [167, 254, 177, 264], [206, 254, 216, 264], [176, 248, 186, 258], [232, 247, 242, 257], [215, 247, 225, 257], [190, 246, 200, 256], [201, 245, 211, 255], [224, 240, 234, 250], [192, 236, 202, 246], [234, 236, 244, 246], [218, 231, 228, 241], [201, 231, 211, 241], [261, 230, 271, 240], [243, 229, 253, 239], [229, 227, 239, 237], [252, 224, 262, 234], [219, 221, 229, 231], [243, 219, 253, 229], [260, 217, 270, 227], [228, 216, 238, 226], [270, 213, 280, 223], [238, 210, 248, 220], [252, 210, 262, 220], [301, 206, 311, 216], [279, 206, 289, 216], [255, 200, 265, 210], [285, 191, 295, 201], [262, 191, 272, 201], [306, 187, 316, 197], [296, 184, 306, 194], [331, 182, 341, 192], [315, 182, 325, 192], [323, 175, 333, 185], [297, 172, 307, 182], [289, 164, 299, 174], [305, 164, 315, 174], [329, 163, 339, 173], [353, 156, 363, 166], [337, 156, 347, 166], [306, 154, 316, 164], [362, 150, 372, 160], [381, 149, 391, 159], [315, 147, 325, 157], [340, 143, 350, 153], [349, 136, 359, 146], [333, 134, 343, 144], [368, 133, 378, 143], [377, 128, 387, 138], [6, 127, 16, 137], [348, 126, 358, 136], [16, 125, 26, 135], [360, 122, 370, 132], [28, 122, 38, 132], [386, 121, 396, 131], [409, 119, 419, 129], [37, 117, 47, 127], [4, 117, 14, 127], [395, 114, 405, 124], [47, 114, 57, 124], [58, 111, 68, 121], [36, 107, 46, 117], [403, 106, 413, 116], [69, 106, 79, 116], [79, 104, 89, 114], [382, 102, 392, 112], [89, 100, 99, 110], [410, 97, 420, 107], [5, 97, 15, 107], [98, 95, 108, 105], [16, 94, 26, 104], [108, 94, 118, 104], [26, 91, 36, 101], [118, 90, 128, 100], [415, 87, 425, 97], [128, 86, 138, 96], [35, 85, 45, 95], [48, 83, 58, 93], [109, 83, 119, 93], [138, 83, 148, 93], [148, 80, 158, 90], [421, 78, 431, 88], [59, 78, 69, 88], [158, 78, 168, 88], [100, 76, 110, 86], [69, 74, 79, 84], [79, 74, 89, 84], [167, 72, 177, 82], [430, 71, 440, 81], [88, 69, 98, 79], [177, 68, 187, 78], [109, 65, 119, 75], [187, 64, 197, 74], [140, 64, 150, 74], [436, 62, 446, 72], [8, 61, 18, 71], [197, 60, 207, 70], [118, 59, 128, 69], [159, 59, 169, 69], [18, 57, 28, 67], [207, 57, 217, 67], [28, 55, 38, 65], [128, 54, 138, 64], [140, 53, 150, 63], [38, 53, 48, 63], [441, 53, 451, 63], [216, 52, 226, 62], [48, 50, 58, 60], [150, 50, 160, 60], [227, 49, 237, 59], [58, 48, 68, 58], [207, 47, 217, 57], [20, 46, 30, 56], [159, 45, 169, 55], [70, 44, 80, 54], [446, 44, 456, 54], [236, 42, 246, 52], [82, 41, 92, 51], [150, 40, 160, 50], [168, 40, 178, 50], [178, 39, 188, 49], [94, 38, 104, 48], [188, 36, 198, 46], [106, 36, 116, 46], [198, 36, 208, 46], [244, 35, 254, 45], [207, 31, 217, 41], [117, 31, 127, 41], [127, 29, 137, 39], [154, 28, 164, 38], [255, 28, 265, 38], [235, 26, 245, 36], [217, 26, 227, 36], [265, 26, 275, 36], [190, 25, 200, 35], [138, 25, 148, 35], [274, 20, 284, 30], [157, 18, 167, 28], [167, 16, 177, 26], [212, 16, 222, 26], [177, 14, 187, 24], [229, 14, 239, 24], [283, 14, 293, 24], [187, 12, 197, 22], [197, 10, 207, 20], [293, 7, 303, 17], [207, 7, 217, 17], [223, 5, 233, 15], [301, 0, 311, 10]], "IMG_02C458E4D7CE-4.jpg": [[180, 471, 190, 481], [165, 471, 175, 481], [166, 461, 176, 471], [181, 461, 191, 471], [166, 451, 176, 461], [181, 451, 191, 461], [184, 441, 194, 451], [168, 441, 178, 451], [185, 431, 195, 441], [169, 431, 179, 441], [171, 421, 181, 431], [186, 421, 196, 431], [170, 411, 180, 421], [187, 411, 197, 421], [173, 401, 183, 411], [188, 401, 198, 411], [174, 391, 184, 401], [190, 391, 200, 401], [193, 381, 203, 391], [175, 381, 185, 391], [176, 371, 186, 381], [194, 371, 204, 381], [178, 361, 188, 371], [195, 360, 205, 370], [180, 351, 190, 361], [196, 350, 206, 360], [182, 341, 192, 351], [196, 340, 206, 350], [182, 331, 192, 341], [198, 330, 208, 340], [184, 321, 194, 331], [200, 320, 210, 330], [185, 311, 195, 321], [201, 310, 211, 320], [188, 301, 198, 311], [204, 300, 214, 310], [198, 291, 208, 301], [206, 284, 216, 294], [190, 284, 200, 294], [205, 274, 215, 284], [191, 274, 201, 284], [206, 264, 216, 274], [194, 264, 204, 274], [199, 254, 209, 264], [207, 246, 217, 256], [197, 239, 207, 249], [211, 235, 221, 245], [185, 230, 195, 240], [198, 227, 208, 237], [212, 225, 222, 235], [194, 217, 204, 227], [217, 216, 227, 226], [194, 207, 204, 217], [218, 206, 228, 216], [220, 196, 230, 206], [195, 196, 205, 206], [186, 187, 196, 197], [222, 186, 232, 196], [196, 186, 206, 196], [176, 178, 186, 188], [203, 177, 213, 187], [218, 176, 228, 186], [280, 170, 290, 180], [196, 169, 206, 179], [179, 168, 189, 178], [213, 167, 223, 177], [285, 161, 295, 171], [275, 160, 285, 170], [195, 159, 205, 169], [177, 158, 187, 168], [217, 157, 227, 167], [268, 151, 278, 161], [288, 151, 298, 161], [228, 150, 238, 160], [193, 149, 203, 159], [174, 148, 184, 158], [217, 144, 227, 154], [287, 141, 297, 151], [267, 141, 277, 151], [191, 139, 201, 149], [171, 138, 181, 148], [218, 134, 228, 144], [287, 131, 297, 141], [267, 131, 277, 141], [189, 129, 199, 139], [170, 128, 180, 138], [258, 125, 268, 135], [219, 124, 229, 134], [270, 121, 280, 131], [287, 121, 297, 131], [187, 119, 197, 129], [168, 118, 178, 128], [255, 115, 265, 125], [219, 114, 229, 124], [269, 111, 279, 121], [287, 111, 297, 121], [185, 109, 195, 119], [167, 108, 177, 118], [254, 105, 264, 115], [221, 104, 231, 114], [270, 101, 280, 111], [287, 101, 297, 111], [184, 99, 194, 109], [165, 98, 175, 108], [253, 95, 263, 105], [222, 94, 232, 104], [270, 91, 280, 101], [287, 91, 297, 101], [182, 89, 192, 99], [164, 88, 174, 98], [254, 85, 264, 95], [224, 84, 234, 94], [287, 81, 297, 91], [272, 81, 282, 91], [180, 79, 190, 89], [162, 78, 172, 88], [251, 75, 261, 85], [226, 74, 236, 84], [271, 71, 281, 81], [287, 71, 297, 81], [179, 69, 189, 79], [630, 69, 640, 79], [161, 68, 171, 78], [250, 65, 260, 75], [609, 65, 619, 75], [227, 64, 237, 74], [621, 64, 631, 74], [272, 61, 282, 71], [289, 61, 299, 71], [177, 59, 187, 69], [160, 58, 170, 68], [249, 55, 259, 65], [229, 54, 239, 64], [289, 51, 299, 61], [273, 51, 283, 61], [177, 49, 187, 59], [159, 48, 169, 58], [249, 45, 259, 55], [228, 44, 238, 54], [290, 41, 300, 51], [275, 41, 285, 51], [175, 39, 185, 49], [158, 38, 168, 48], [248, 35, 258, 45], [226, 34, 236, 44], [291, 31, 301, 41], [276, 31, 286, 41], [174, 29, 184, 39], [156, 28, 166, 38], [249, 25, 259, 35], [226, 24, 236, 34], [276, 21, 286, 31], [292, 21, 302, 31], [173, 19, 183, 29], [156, 18, 166, 28], [247, 15, 257, 25], [228, 14, 238, 24], [278, 11, 288, 21], [293, 11, 303, 21], [171, 9, 181, 19], [155, 8, 165, 18], [243, 5, 253, 15], [233, 5, 243, 15], [295, 1, 305, 11], [278, 1, 288, 11]], "IMG_02C458E4D7CE-5.jpg": [[185, 475, 195, 485], [258, 474, 268, 484], [269, 472, 279, 482], [551, 472, 561, 482], [195, 471, 205, 481], [531, 471, 541, 481], [595, 470, 605, 480], [279, 468, 289, 478], [211, 467, 221, 477], [449, 467, 459, 477], [517, 464, 527, 474], [221, 463, 231, 473], [288, 463, 298, 473], [544, 463, 554, 473], [269, 462, 279, 472], [590, 461, 600, 471], [231, 460, 241, 470], [298, 459, 308, 469], [241, 458, 251, 468], [444, 458, 454, 468], [256, 455, 266, 465], [309, 454, 319, 464], [519, 454, 529, 464], [537, 454, 547, 464], [607, 454, 617, 464], [266, 452, 276, 462], [291, 451, 301, 461], [586, 451, 596, 461], [326, 450, 336, 460], [276, 448, 286, 458], [439, 448, 449, 458], [353, 447, 363, 457], [335, 445, 345, 455], [532, 445, 542, 455], [317, 444, 327, 454], [578, 444, 588, 454], [513, 443, 523, 453], [295, 441, 305, 451], [305, 438, 315, 448], [343, 438, 353, 448], [435, 438, 445, 448], [587, 438, 597, 448], [525, 437, 535, 447], [572, 435, 582, 445], [497, 434, 507, 444], [320, 434, 330, 444], [513, 433, 523, 443], [351, 431, 361, 441], [331, 430, 341, 440], [589, 428, 599, 438], [433, 428, 443, 438], [363, 427, 373, 437], [567, 426, 577, 436], [507, 424, 517, 434], [373, 423, 383, 433], [387, 419, 397, 429], [588, 418, 598, 428], [500, 416, 510, 426], [566, 416, 576, 426], [400, 415, 410, 425], [412, 411, 422, 421], [370, 409, 380, 419], [493, 407, 503, 417], [427, 406, 437, 416], [565, 406, 575, 416], [436, 401, 446, 411], [486, 398, 496, 408], [446, 397, 456, 407], [564, 396, 574, 406], [456, 393, 466, 403], [425, 390, 435, 400], [480, 388, 490, 398], [465, 387, 475, 397], [560, 386, 570, 396], [490, 384, 500, 394], [521, 384, 531, 394], [421, 380, 431, 390], [505, 379, 515, 389], [552, 378, 562, 388], [532, 375, 542, 385], [542, 373, 552, 383], [472, 373, 482, 383], [512, 371, 522, 381], [421, 370, 431, 380], [497, 369, 507, 379], [465, 365, 475, 375], [419, 360, 429, 370], [458, 357, 468, 367], [421, 350, 431, 360], [451, 349, 461, 359], [443, 341, 453, 351], [425, 340, 435, 350], [436, 333, 446, 343], [432, 323, 442, 333], [427, 314, 437, 324], [1, 82, 11, 92], [11, 80, 21, 90], [21, 78, 31, 88], [32, 75, 42, 85], [42, 70, 52, 80], [51, 64, 61, 74], [60, 58, 70, 68], [19, 55, 29, 65], [70, 54, 80, 64], [9, 53, 19, 63], [80, 51, 90, 61], [57, 48, 67, 58], [90, 48, 100, 58], [100, 47, 110, 57], [175, 47, 185, 57], [117, 45, 127, 55], [138, 41, 148, 51], [47, 41, 57, 51], [151, 40, 161, 50], [126, 40, 136, 50], [167, 40, 177, 50], [25, 37, 35, 47], [37, 37, 47, 47], [110, 35, 120, 45], [145, 30, 155, 40], [67, 12, 77, 22], [53, 11, 63, 21], [79, 8, 89, 18], [44, 6, 54, 16], [631, 6, 641, 16], [94, 3, 104, 13], [144, 1, 154, 11], [35, 0, 45, 10]], "IMG_02C458E4D7CE-6.jpg": [[29, 475, 39, 485], [468, 475, 478, 485], [45, 474, 55, 484], [447, 474, 457, 484], [58, 472, 68, 482], [592, 472, 602, 482], [619, 471, 629, 481], [473, 466, 483, 476], [452, 465, 462, 475], [32, 465, 42, 475], [48, 464, 58, 474], [612, 463, 622, 473], [60, 462, 70, 472], [463, 459, 473, 469], [478, 457, 488, 467], [33, 455, 43, 465], [49, 454, 59, 464], [611, 453, 621, 463], [62, 452, 72, 462], [461, 449, 471, 459], [621, 449, 631, 459], [483, 448, 493, 458], [33, 445, 43, 455], [51, 444, 61, 454], [608, 443, 618, 453], [63, 442, 73, 452], [466, 440, 476, 450], [627, 440, 637, 450], [488, 439, 498, 449], [34, 435, 44, 445], [54, 434, 64, 444], [66, 432, 76, 442], [472, 431, 482, 441], [634, 431, 644, 441], [494, 430, 504, 440], [604, 430, 614, 440], [484, 428, 494, 438], [622, 427, 632, 437], [37, 425, 47, 435], [54, 424, 64, 434], [68, 422, 78, 432], [610, 421, 620, 431], [500, 421, 510, 431], [479, 419, 489, 429], [623, 417, 633, 427], [37, 415, 47, 425], [56, 414, 66, 424], [70, 412, 80, 422], [505, 412, 515, 422], [607, 411, 617, 421], [484, 410, 494, 420], [628, 408, 638, 418], [38, 405, 48, 415], [57, 404, 67, 414], [72, 402, 82, 412], [509, 402, 519, 412], [489, 401, 499, 411], [611, 401, 621, 411], [632, 398, 642, 408], [39, 395, 49, 405], [59, 394, 69, 404], [502, 394, 512, 404], [615, 391, 625, 401], [516, 390, 526, 400], [76, 389, 86, 399], [39, 385, 49, 395], [60, 384, 70, 394], [500, 384, 510, 394], [620, 382, 630, 392], [521, 381, 531, 391], [78, 379, 88, 389], [41, 375, 51, 385], [62, 374, 72, 384], [505, 374, 515, 384], [625, 373, 635, 383], [528, 372, 538, 382], [516, 372, 526, 382], [80, 369, 90, 379], [42, 365, 52, 375], [63, 364, 73, 374], [533, 363, 543, 373], [630, 362, 640, 372], [513, 360, 523, 370], [83, 359, 93, 369], [44, 355, 54, 365], [65, 354, 75, 364], [538, 353, 548, 363], [633, 352, 643, 362], [519, 351, 529, 361], [84, 349, 94, 359], [264, 346, 274, 356], [46, 345, 56, 355], [533, 344, 543, 354], [67, 344, 77, 354], [543, 343, 553, 353], [273, 339, 283, 349], [81, 339, 91, 349], [49, 335, 59, 345], [528, 335, 538, 345], [67, 334, 77, 344], [538, 334, 548, 344], [550, 334, 560, 344], [281, 332, 291, 342], [87, 330, 97, 340], [290, 327, 300, 337], [50, 325, 60, 335], [546, 324, 556, 334], [70, 324, 80, 334], [536, 324, 546, 334], [556, 322, 566, 332], [87, 320, 97, 330], [297, 319, 307, 329], [50, 315, 60, 325], [543, 314, 553, 324], [71, 314, 81, 324], [306, 314, 316, 324], [93, 311, 103, 321], [555, 309, 565, 319], [53, 305, 63, 315], [72, 304, 82, 314], [88, 302, 98, 312], [577, 301, 587, 311], [559, 299, 569, 309], [54, 295, 64, 305], [74, 294, 84, 304], [94, 293, 104, 303], [565, 290, 575, 300], [56, 285, 66, 295], [76, 284, 86, 294], [95, 283, 105, 293], [570, 281, 580, 291], [206, 276, 216, 286], [56, 275, 66, 285], [78, 274, 88, 284], [98, 273, 108, 283], [575, 272, 585, 282], [204, 266, 214, 276], [58, 265, 68, 275], [79, 264, 89, 274], [101, 263, 111, 273], [580, 263, 590, 273], [198, 256, 208, 266], [60, 255, 70, 265], [82, 254, 92, 264], [585, 254, 595, 264], [102, 253, 112, 263], [189, 251, 199, 261], [228, 250, 238, 260], [173, 246, 183, 256], [201, 246, 211, 256], [59, 245, 69, 255], [590, 245, 600, 255], [83, 244, 93, 254], [103, 243, 113, 253], [183, 241, 193, 251], [221, 241, 231, 251], [575, 240, 585, 250], [201, 236, 211, 246], [595, 236, 605, 246], [62, 235, 72, 245], [83, 234, 93, 244], [213, 234, 223, 244], [106, 233, 116, 243], [178, 232, 188, 242], [156, 229, 166, 239], [203, 226, 213, 236], [600, 226, 610, 236], [64, 225, 74, 235], [86, 224, 96, 234], [107, 222, 117, 232], [175, 222, 185, 232], [164, 221, 174, 231], [5, 218, 15, 228], [198, 217, 208, 227], [605, 217, 615, 227], [143, 216, 153, 226], [67, 215, 77, 225], [87, 214, 97, 224], [14, 213, 24, 223], [157, 213, 167, 223], [180, 213, 190, 223], [108, 212, 118, 222], [192, 208, 202, 218], [611, 208, 621, 218], [23, 207, 33, 217], [171, 207, 181, 217], [149, 206, 159, 216], [68, 205, 78, 215], [88, 204, 98, 214], [181, 203, 191, 213], [111, 202, 121, 212], [32, 201, 42, 211], [616, 199, 626, 209], [170, 197, 180, 207], [41, 195, 51, 205], [71, 195, 81, 205], [145, 195, 155, 205], [188, 195, 198, 205], [90, 194, 100, 204], [111, 192, 121, 202], [621, 190, 631, 200], [179, 190, 189, 200], [50, 189, 60, 199], [167, 187, 177, 197], [140, 186, 150, 196], [92, 184, 102, 194], [70, 184, 80, 194], [113, 182, 123, 192], [58, 182, 68, 192], [186, 182, 196, 192], [176, 180, 186, 190], [625, 180, 635, 190], [166, 177, 176, 187], [137, 176, 147, 186], [74, 174, 84, 184], [91, 173, 101, 183], [183, 172, 193, 182], [114, 171, 124, 181], [630, 171, 640, 181], [162, 167, 172, 177], [134, 166, 144, 176], [81, 165, 91, 175], [177, 163, 187, 173], [115, 161, 125, 171], [149, 158, 159, 168], [131, 156, 141, 166], [86, 155, 96, 165], [177, 153, 187, 163], [119, 151, 129, 161], [154, 149, 164, 159], [89, 145, 99, 155], [174, 143, 184, 153], [120, 141, 130, 151], [153, 138, 163, 148], [109, 137, 119, 147], [94, 136, 104, 146], [171, 133, 181, 143], [119, 131, 129, 141], [150, 128, 160, 138], [91, 126, 101, 136], [171, 123, 181, 133], [122, 121, 132, 131], [489, 119, 499, 129], [148, 118, 158, 128], [93, 116, 103, 126], [169, 113, 179, 123], [126, 111, 136, 121], [494, 110, 504, 120], [504, 110, 514, 120], [101, 108, 111, 118], [143, 108, 153, 118], [91, 106, 101, 116], [166, 103, 176, 113], [121, 102, 131, 112], [512, 102, 522, 112], [108, 100, 118, 110], [495, 100, 505, 110], [142, 98, 152, 108], [89, 96, 99, 106], [165, 93, 175, 103], [519, 93, 529, 103], [111, 90, 121, 100], [498, 90, 508, 100], [147, 88, 157, 98], [94, 87, 104, 97], [162, 83, 172, 93], [524, 83, 534, 93], [502, 80, 512, 90], [107, 80, 117, 90], [151, 78, 161, 88], [529, 74, 539, 84], [508, 71, 518, 81], [107, 70, 117, 80], [153, 68, 163, 78], [534, 65, 544, 75], [512, 61, 522, 71], [110, 60, 120, 70], [154, 58, 164, 68], [538, 54, 548, 64], [164, 52, 174, 62], [516, 51, 526, 61], [111, 50, 121, 60], [153, 48, 163, 58], [543, 44, 553, 54], [171, 43, 181, 53], [521, 42, 531, 52], [111, 40, 121, 50], [151, 38, 161, 48], [546, 34, 556, 44], [174, 33, 184, 43], [525, 32, 535, 42], [112, 30, 122, 40], [149, 28, 159, 38], [551, 25, 561, 35], [530, 23, 540, 33], [177, 23, 187, 33], [117, 21, 127, 31], [143, 19, 153, 29], [555, 15, 565, 25], [535, 13, 545, 23], [179, 13, 189, 23], [122, 12, 132, 22], [138, 10, 148, 20], [559, 5, 569, 15], [540, 3, 550, 13], [182, 3, 192, 13], [125, 2, 135, 12]], "IMG_02C458E4D7CE-7.jpg": [[322, 475, 332, 485], [291, 470, 301, 480], [314, 468, 324, 478], [303, 468, 313, 478], [282, 464, 292, 474], [160, 462, 170, 472], [304, 458, 314, 468], [274, 456, 284, 466], [149, 455, 159, 465], [159, 452, 169, 462], [139, 451, 149, 461], [295, 451, 305, 461], [264, 449, 274, 459], [130, 446, 140, 456], [147, 444, 157, 454], [287, 443, 297, 453], [117, 442, 127, 452], [256, 441, 266, 451], [108, 436, 118, 446], [93, 435, 103, 445], [280, 435, 290, 445], [83, 434, 93, 444], [248, 434, 258, 444], [72, 432, 82, 442], [28, 431, 38, 441], [62, 429, 72, 439], [18, 428, 28, 438], [41, 428, 51, 438], [271, 428, 281, 438], [8, 427, 18, 437], [240, 427, 250, 437], [52, 427, 62, 437], [222, 422, 232, 432], [180, 422, 190, 432], [262, 421, 272, 431], [35, 418, 45, 428], [236, 416, 246, 426], [19, 415, 29, 425], [171, 415, 181, 425], [254, 414, 264, 424], [6, 413, 16, 423], [224, 412, 234, 422], [164, 407, 174, 417], [244, 406, 254, 416], [119, 405, 129, 415], [215, 405, 225, 415], [96, 403, 106, 413], [86, 402, 96, 412], [71, 402, 81, 412], [109, 401, 119, 411], [155, 400, 165, 410], [236, 399, 246, 409], [139, 397, 149, 407], [206, 397, 216, 407], [52, 396, 62, 406], [62, 396, 72, 406], [17, 394, 27, 404], [41, 394, 51, 404], [29, 394, 39, 404], [126, 394, 136, 404], [7, 392, 17, 402], [84, 392, 94, 402], [228, 392, 238, 402], [108, 391, 118, 401], [198, 389, 208, 399], [185, 388, 195, 398], [208, 387, 218, 397], [98, 386, 108, 396], [220, 384, 230, 394], [63, 383, 73, 393], [45, 382, 55, 392], [74, 381, 84, 391], [88, 381, 98, 391], [185, 378, 195, 388], [196, 378, 206, 388], [36, 377, 46, 387], [212, 377, 222, 387], [26, 376, 36, 386], [6, 376, 16, 386], [16, 375, 26, 385], [177, 370, 187, 380], [203, 370, 213, 380], [192, 367, 202, 377], [159, 363, 169, 373], [170, 362, 180, 372], [191, 357, 201, 367], [179, 356, 189, 366], [159, 353, 169, 363], [147, 350, 157, 360], [126, 349, 136, 359], [177, 346, 187, 356], [119, 340, 129, 350], [169, 339, 179, 349], [153, 336, 163, 346], [7, 334, 17, 344], [18, 333, 28, 343], [112, 332, 122, 342], [35, 329, 45, 339], [102, 329, 112, 339], [144, 328, 154, 338], [45, 325, 55, 335], [110, 322, 120, 332], [94, 321, 104, 331], [55, 320, 65, 330], [136, 320, 146, 330], [65, 317, 75, 327], [75, 315, 85, 325], [628, 314, 638, 324], [85, 312, 95, 322], [128, 312, 138, 322], [95, 309, 105, 319], [621, 306, 631, 316], [119, 305, 129, 315], [138, 302, 148, 312], [103, 301, 113, 311], [615, 297, 625, 307], [147, 295, 157, 305], [129, 295, 139, 305], [572, 295, 582, 305], [103, 291, 113, 301], [607, 290, 617, 300], [562, 290, 572, 300], [155, 288, 165, 298], [551, 287, 561, 297], [632, 287, 642, 297], [541, 286, 551, 296], [165, 285, 175, 295], [144, 285, 154, 295], [96, 283, 106, 293], [176, 282, 186, 292], [600, 282, 610, 292], [196, 280, 206, 290], [529, 279, 539, 289], [625, 279, 635, 289], [186, 278, 196, 288], [207, 276, 217, 286], [217, 276, 227, 286], [89, 275, 99, 285], [518, 275, 528, 285], [593, 273, 603, 283], [550, 272, 560, 282], [226, 271, 236, 281], [196, 270, 206, 280], [101, 270, 111, 280], [619, 270, 629, 280], [81, 268, 91, 278], [511, 267, 521, 277], [586, 264, 596, 274], [234, 263, 244, 273], [502, 262, 512, 272], [546, 262, 556, 272], [611, 262, 621, 272], [73, 261, 83, 271], [244, 259, 254, 269], [511, 256, 521, 266], [578, 256, 588, 266], [489, 254, 499, 264], [254, 253, 264, 263], [65, 253, 75, 263], [43, 253, 53, 263], [604, 253, 614, 263], [542, 252, 552, 262], [614, 251, 624, 261], [264, 249, 274, 259], [498, 249, 508, 259], [571, 248, 581, 258], [516, 247, 526, 257], [57, 245, 67, 255], [274, 245, 284, 255], [597, 245, 607, 255], [488, 244, 498, 254], [284, 240, 294, 250], [564, 240, 574, 250], [589, 237, 599, 247], [49, 237, 59, 247], [480, 236, 490, 246], [304, 236, 314, 246], [502, 235, 512, 245], [294, 235, 304, 245], [63, 232, 73, 242], [314, 232, 324, 242], [557, 232, 567, 242], [630, 230, 640, 240], [41, 229, 51, 239], [582, 229, 592, 239], [475, 227, 485, 237], [498, 225, 508, 235], [307, 224, 317, 234], [550, 224, 560, 234], [622, 222, 632, 232], [33, 221, 43, 231], [343, 220, 353, 230], [575, 220, 585, 230], [468, 218, 478, 228], [490, 218, 500, 228], [352, 215, 362, 225], [544, 215, 554, 225], [25, 214, 35, 224], [614, 214, 624, 224], [568, 211, 578, 221], [361, 210, 371, 220], [553, 209, 563, 219], [461, 209, 471, 219], [483, 209, 493, 219], [371, 207, 381, 217], [16, 206, 26, 216], [538, 206, 548, 216], [606, 205, 616, 215], [561, 202, 571, 212], [630, 202, 640, 212], [383, 201, 393, 211], [455, 200, 465, 210], [477, 200, 487, 210], [465, 199, 475, 209], [8, 198, 18, 208], [531, 198, 541, 208], [369, 197, 379, 207], [393, 197, 403, 207], [543, 197, 553, 207], [599, 197, 609, 207], [553, 195, 563, 205], [622, 194, 632, 204], [447, 193, 457, 203], [504, 192, 514, 202], [402, 191, 412, 201], [413, 189, 423, 199], [469, 189, 479, 199], [592, 189, 602, 199], [546, 187, 556, 197], [455, 186, 465, 196], [614, 186, 624, 196], [440, 185, 450, 195], [422, 183, 432, 193], [501, 182, 511, 192], [527, 180, 537, 190], [606, 178, 616, 188], [6, 177, 16, 187], [463, 176, 473, 186], [433, 175, 443, 185], [589, 174, 599, 184], [520, 172, 530, 182], [472, 171, 482, 181], [634, 171, 644, 181], [572, 168, 582, 178], [455, 168, 465, 178], [426, 167, 436, 177], [441, 167, 451, 177], [506, 166, 516, 176], [481, 165, 491, 175], [493, 163, 503, 173], [629, 162, 639, 172], [564, 161, 574, 171], [448, 159, 458, 169], [418, 159, 428, 169], [466, 158, 476, 168], [584, 156, 594, 166], [476, 155, 486, 165], [622, 153, 632, 163], [557, 152, 567, 162], [411, 151, 421, 161], [441, 151, 451, 161], [492, 147, 502, 157], [578, 147, 588, 157], [470, 146, 480, 156], [616, 144, 626, 154], [434, 143, 444, 153], [404, 143, 414, 153], [557, 140, 567, 150], [509, 139, 519, 149], [570, 139, 580, 149], [467, 136, 477, 146], [609, 136, 619, 146], [397, 135, 407, 145], [427, 135, 437, 145], [477, 135, 487, 145], [524, 134, 534, 144], [549, 132, 559, 142], [635, 131, 645, 141], [497, 130, 507, 140], [459, 128, 469, 138], [486, 128, 496, 138], [419, 127, 429, 137], [390, 127, 400, 137], [506, 125, 516, 135], [519, 125, 529, 135], [541, 124, 551, 134], [627, 123, 637, 133], [452, 120, 462, 130], [383, 119, 393, 129], [411, 118, 421, 128], [487, 118, 497, 128], [426, 116, 436, 126], [534, 116, 544, 126], [552, 116, 562, 126], [610, 116, 620, 126], [444, 112, 454, 122], [376, 110, 386, 120], [404, 110, 414, 120], [480, 109, 490, 119], [560, 109, 570, 119], [525, 108, 535, 118], [602, 108, 612, 118], [420, 107, 430, 117], [437, 103, 447, 113], [397, 102, 407, 112], [368, 102, 378, 112], [489, 102, 499, 112], [569, 102, 579, 112], [473, 101, 483, 111], [518, 100, 528, 110], [595, 100, 605, 110], [428, 96, 438, 106], [233, 94, 243, 104], [361, 94, 371, 104], [389, 94, 399, 104], [466, 93, 476, 103], [510, 93, 520, 103], [564, 93, 574, 103], [591, 89, 601, 99], [421, 88, 431, 98], [493, 88, 503, 98], [187, 87, 197, 97], [354, 86, 364, 96], [381, 86, 391, 96], [226, 85, 236, 95], [458, 85, 468, 95], [558, 84, 568, 94], [605, 82, 615, 92], [485, 81, 495, 91], [413, 80, 423, 90], [591, 79, 601, 89], [347, 78, 357, 88], [373, 78, 383, 88], [181, 77, 191, 87], [218, 77, 228, 87], [614, 77, 624, 87], [624, 77, 634, 87], [505, 77, 515, 87], [204, 76, 214, 86], [453, 76, 463, 86], [552, 75, 562, 85], [395, 73, 405, 83], [406, 71, 416, 81], [340, 70, 350, 80], [366, 70, 376, 80], [586, 70, 596, 80], [173, 69, 183, 79], [355, 69, 365, 79], [500, 68, 510, 78], [444, 68, 454, 78], [209, 67, 219, 77], [546, 66, 556, 76], [399, 63, 409, 73], [165, 62, 175, 72], [333, 62, 343, 72], [581, 61, 591, 71], [437, 60, 447, 70], [479, 60, 489, 70], [492, 60, 502, 70], [201, 59, 211, 69], [357, 58, 367, 68], [539, 58, 549, 68], [157, 55, 167, 65], [392, 55, 402, 65], [421, 54, 431, 64], [471, 53, 481, 63], [192, 52, 202, 62], [576, 52, 586, 62], [531, 50, 541, 60], [350, 49, 360, 59], [385, 47, 395, 57], [152, 46, 162, 56], [464, 45, 474, 55], [184, 45, 194, 55], [423, 44, 433, 54], [168, 44, 178, 54], [330, 43, 340, 53], [570, 43, 580, 53], [341, 42, 351, 52], [525, 41, 535, 51], [377, 39, 387, 49], [145, 38, 155, 48], [457, 37, 467, 47], [367, 36, 377, 46], [416, 36, 426, 46], [322, 35, 332, 45], [563, 34, 573, 44], [518, 33, 528, 43], [137, 31, 147, 41], [314, 27, 324, 37], [411, 27, 421, 37], [452, 27, 462, 37], [632, 27, 642, 37], [364, 26, 374, 36], [557, 25, 567, 35], [511, 24, 521, 34], [127, 23, 137, 33], [445, 19, 455, 29], [626, 18, 636, 28], [307, 18, 317, 28], [118, 18, 128, 28], [552, 16, 562, 26], [438, 11, 448, 21], [110, 10, 120, 20], [628, 8, 638, 18], [544, 8, 554, 18], [426, 4, 436, 14], [413, 2, 423, 12], [101, 2, 111, 12], [535, 2, 545, 12]], "IMG_02C458E4D7CE-9.jpg": [[535, 475, 545, 485], [493, 473, 503, 483], [544, 469, 554, 479], [511, 467, 521, 477], [575, 464, 585, 474], [521, 464, 531, 474], [498, 463, 508, 473], [564, 460, 574, 470], [530, 459, 540, 469], [549, 457, 559, 467], [577, 454, 587, 464], [588, 450, 598, 460], [556, 448, 566, 458], [612, 447, 622, 457], [542, 446, 552, 456], [598, 446, 608, 456], [580, 443, 590, 453], [565, 442, 575, 452], [620, 440, 630, 450], [607, 436, 617, 446], [630, 434, 640, 444], [584, 433, 594, 443], [594, 430, 604, 440], [604, 426, 614, 436], [613, 420, 623, 430], [623, 419, 633, 429], [631, 412, 641, 422], [112, 350, 122, 360], [124, 342, 134, 352], [114, 340, 124, 350], [117, 330, 127, 340], [108, 322, 118, 332], [118, 318, 128, 328], [160, 316, 170, 326], [106, 312, 116, 322], [158, 306, 168, 316], [128, 306, 138, 316], [114, 305, 124, 315], [155, 296, 165, 306], [116, 295, 126, 305], [150, 287, 160, 297], [113, 285, 123, 295], [123, 281, 133, 291], [145, 277, 155, 287], [108, 276, 118, 286], [130, 273, 140, 283], [103, 267, 113, 277], [146, 266, 156, 276], [131, 261, 141, 271], [101, 257, 111, 267], [126, 252, 136, 262], [100, 247, 110, 257], [88, 245, 98, 255], [123, 242, 133, 252], [95, 237, 105, 247], [121, 232, 131, 242], [355, 230, 365, 240], [93, 227, 103, 237], [119, 222, 129, 232], [357, 220, 367, 230], [74, 220, 84, 230], [626, 219, 636, 229], [89, 217, 99, 227], [606, 215, 616, 225], [616, 215, 626, 225], [366, 214, 376, 224], [119, 212, 129, 222], [596, 210, 606, 220], [81, 210, 91, 220], [375, 209, 385, 219], [92, 207, 102, 217], [565, 207, 575, 217], [582, 206, 592, 216], [384, 203, 394, 213], [118, 202, 128, 212], [74, 201, 84, 211], [394, 199, 404, 209], [632, 199, 642, 209], [550, 198, 560, 208], [560, 198, 570, 208], [95, 197, 105, 207], [404, 196, 414, 206], [418, 194, 428, 204], [533, 194, 543, 204], [353, 194, 363, 204], [611, 192, 621, 202], [119, 192, 129, 202], [621, 192, 631, 202], [70, 191, 80, 201], [428, 190, 438, 200], [523, 190, 533, 200], [601, 188, 611, 198], [92, 187, 102, 197], [361, 187, 371, 197], [438, 187, 448, 197], [509, 185, 519, 195], [585, 184, 595, 194], [65, 182, 75, 192], [116, 182, 126, 192], [575, 181, 585, 191], [519, 180, 529, 190], [447, 180, 457, 190], [369, 179, 379, 189], [500, 179, 510, 189], [563, 178, 573, 188], [91, 177, 101, 187], [457, 177, 467, 187], [467, 176, 477, 186], [553, 174, 563, 184], [478, 173, 488, 183], [60, 172, 70, 182], [115, 172, 125, 182], [526, 171, 536, 181], [488, 170, 498, 180], [376, 170, 386, 180], [442, 170, 452, 180], [543, 169, 553, 179], [88, 167, 98, 177], [498, 167, 508, 177], [513, 167, 523, 177], [433, 163, 443, 173], [451, 163, 461, 173], [55, 162, 65, 172], [113, 162, 123, 172], [383, 161, 393, 171], [551, 161, 561, 171], [373, 160, 383, 170], [484, 160, 494, 170], [85, 157, 95, 167], [475, 155, 485, 165], [427, 154, 437, 164], [391, 154, 401, 164], [50, 153, 60, 163], [417, 153, 427, 163], [403, 152, 413, 162], [112, 152, 122, 162], [438, 151, 448, 161], [464, 151, 474, 161], [452, 150, 462, 160], [65, 148, 75, 158], [83, 147, 93, 157], [384, 146, 394, 156], [45, 143, 55, 153], [74, 142, 84, 152], [112, 142, 122, 152], [85, 136, 95, 146], [67, 133, 77, 143], [40, 133, 50, 143], [111, 132, 121, 142], [86, 126, 96, 136], [62, 123, 72, 133], [35, 123, 45, 133], [109, 122, 119, 132], [85, 116, 95, 126], [57, 114, 67, 124], [108, 112, 118, 122], [30, 112, 40, 122], [85, 106, 95, 116], [52, 104, 62, 114], [25, 102, 35, 112], [105, 102, 115, 112], [84, 96, 94, 106], [47, 94, 57, 104], [106, 92, 116, 102], [21, 92, 31, 102], [83, 86, 93, 96], [42, 84, 52, 94], [16, 83, 26, 93], [104, 81, 114, 91], [82, 76, 92, 86], [37, 75, 47, 85], [11, 73, 21, 83], [103, 71, 113, 81], [81, 66, 91, 76], [8, 63, 18, 73], [31, 62, 41, 72], [103, 61, 113, 71], [626, 60, 636, 70], [614, 58, 624, 68], [80, 56, 90, 66], [595, 56, 605, 66], [3, 54, 13, 64], [585, 54, 595, 64], [26, 52, 36, 62], [572, 51, 582, 61], [102, 51, 112, 61], [558, 47, 568, 57], [81, 46, 91, 56], [545, 45, 555, 55], [20, 43, 30, 53], [530, 43, 540, 53], [101, 41, 111, 51], [2, 39, 12, 49], [79, 36, 89, 46], [101, 31, 111, 41], [78, 26, 88, 36], [100, 21, 110, 31], [78, 15, 88, 25], [96, 11, 106, 21], [83, 6, 93, 16]]} \ No newline at end of file diff --git a/datasets/train_seg_points.json b/datasets/train_seg_points.json new file mode 100644 index 0000000000000000000000000000000000000000..bdb83e755951e3af95f3f9d48f7ebd4b93ddc30b --- /dev/null +++ b/datasets/train_seg_points.json @@ -0,0 +1 @@ +{"230219_A191_1.jpg": [[126, 414], [124, 407], [149, 399], [125, 396], [142, 394], [126, 385], [121, 376], [117, 367], [113, 357], [108, 348], [103, 338], [96, 329], [89, 322], [82, 313], [75, 304], [70, 297], [559, 253], [555, 247], [549, 238], [545, 229], [539, 219], [534, 210], [529, 201], [524, 193], [519, 182], [515, 174], [509, 164], [504, 155], [497, 146], [491, 136], [485, 130], [478, 120], [472, 111], [465, 102], [461, 95], [448, 92], [442, 87], [432, 80], [464, 77], [422, 74], [463, 69], [415, 67], [461, 59], [409, 58], [459, 49], [404, 49], [388, 9], [409, 6], [400, 8]], "230219_A191_1_test.jpg": [[126, 414], [124, 407], [149, 399], [125, 396], [142, 394], [126, 385], [121, 376], [117, 367], [113, 357], [108, 348], [103, 338], [96, 329], [89, 322], [82, 313], [75, 304], [70, 297], [559, 253], [555, 247], [549, 238], [545, 229], [539, 219], [534, 210], [529, 201], [524, 193], [519, 182], [515, 174], [509, 164], [504, 155], [497, 146], [491, 136], [485, 130], [478, 120], [472, 111], [465, 102], [461, 95], [448, 92], [442, 87], [432, 80], [464, 77], [422, 74], [463, 69], [415, 67], [461, 59], [409, 58], [459, 49], [404, 49], [388, 9], [409, 6], [400, 8]], "230219_A200_4.jpg": [[620, 413], [595, 409], [615, 408], [297, 404], [607, 400], [297, 392], [599, 392], [594, 384], [297, 379], [586, 374], [294, 370], [579, 365], [287, 361], [573, 356], [282, 352], [568, 348], [276, 343], [561, 340], [270, 334], [554, 332], [264, 324], [548, 323], [258, 316], [541, 314], [252, 307], [534, 304], [246, 298], [528, 297], [521, 289], [240, 289], [515, 281], [234, 281], [299, 277], [289, 272], [228, 271], [508, 271], [282, 266], [222, 263], [503, 262], [273, 259], [217, 253], [498, 252], [265, 251], [210, 245], [256, 244], [492, 245], [247, 238], [205, 235], [238, 232], [214, 227], [199, 228], [229, 225], [193, 218], [219, 214], [197, 210], [188, 208], [180, 199], [176, 193], [163, 186], [156, 180], [176, 177], [152, 170], [175, 172], [581, 48], [595, 44], [587, 39], [580, 31], [191, 29], [574, 23], [184, 22], [566, 14], [179, 13], [559, 6], [173, 5]], "IMG_02C458E4D7CE-3.jpg": [[420, 478], [8, 472], [428, 470], [438, 469], [17, 466], [26, 461], [438, 459], [449, 455], [37, 454], [19, 453], [44, 448], [9, 447], [53, 441], [18, 442], [42, 441], [28, 436], [63, 436], [78, 435], [48, 430], [93, 424], [7, 422], [52, 423], [71, 421], [104, 418], [15, 416], [78, 416], [24, 409], [87, 410], [14, 407], [101, 403], [79, 398], [109, 398], [123, 390], [86, 391], [12, 388], [30, 387], [115, 382], [39, 381], [48, 376], [75, 371], [56, 370], [66, 363], [12, 359], [74, 354], [38, 353], [26, 352], [5, 350], [82, 347], [15, 346], [45, 343], [60, 340], [92, 340], [25, 338], [100, 333], [113, 327], [83, 328], [31, 324], [6, 327], [120, 321], [95, 319], [38, 320], [73, 318], [58, 316], [46, 316], [128, 314], [81, 312], [25, 310], [140, 307], [90, 307], [64, 304], [99, 303], [126, 302], [79, 300], [147, 301], [68, 299], [108, 298], [51, 292], [156, 295], [119, 292], [76, 290], [38, 290], [63, 288], [165, 289], [125, 287], [157, 283], [137, 282], [174, 284], [196, 280], [60, 279], [145, 278], [168, 276], [183, 276], [155, 272], [192, 271], [60, 272], [202, 265], [163, 266], [180, 264], [227, 261], [189, 258], [171, 259], [210, 260], [179, 253], [236, 251], [220, 252], [194, 251], [206, 250], [228, 245], [196, 240], [238, 240], [223, 236], [205, 235], [265, 234], [246, 235], [232, 231], [257, 229], [223, 226], [246, 222], [264, 222], [232, 220], [276, 216], [242, 214], [256, 214], [304, 209], [283, 210], [259, 205], [290, 194], [266, 196], [311, 191], [299, 189], [335, 186], [319, 187], [326, 181], [304, 175], [294, 169], [310, 169], [334, 166], [359, 159], [340, 162], [310, 158], [367, 153], [387, 152], [320, 151], [346, 146], [353, 141], [338, 139], [375, 136], [382, 132], [10, 131], [353, 131], [20, 129], [364, 126], [32, 126], [390, 126], [413, 123], [42, 121], [8, 121], [399, 119], [51, 118], [62, 115], [41, 110], [407, 111], [73, 110], [83, 108], [386, 106], [93, 105], [413, 102], [10, 101], [102, 101], [20, 98], [112, 98], [30, 95], [123, 94], [420, 91], [132, 91], [39, 90], [52, 87], [114, 88], [142, 88], [151, 84], [426, 82], [63, 82], [163, 81], [105, 80], [73, 79], [84, 78], [171, 77], [435, 75], [92, 73], [181, 72], [113, 68], [191, 68], [144, 67], [441, 67], [13, 65], [201, 64], [122, 63], [164, 61], [22, 62], [211, 61], [33, 59], [133, 58], [144, 57], [42, 57], [446, 57], [220, 57], [52, 54], [155, 54], [231, 53], [62, 52], [211, 51], [24, 49], [163, 49], [74, 48], [450, 49], [241, 47], [86, 45], [154, 45], [173, 44], [181, 43], [98, 42], [193, 40], [110, 40], [203, 39], [249, 38], [213, 34], [121, 35], [130, 33], [157, 31], [259, 32], [238, 29], [220, 30], [269, 30], [194, 29], [142, 29], [278, 25], [161, 22], [171, 20], [216, 20], [180, 19], [232, 16], [287, 19], [191, 16], [201, 14], [296, 12], [211, 12], [227, 10], [306, 5]], "IMG_02C458E4D7CE-4.jpg": [[184, 475], [169, 475], [170, 465], [185, 465], [170, 455], [186, 455], [188, 445], [173, 444], [190, 435], [174, 436], [175, 425], [191, 425], [175, 415], [192, 415], [177, 405], [193, 404], [178, 395], [194, 395], [197, 385], [180, 385], [180, 375], [198, 375], [182, 365], [199, 364], [184, 355], [200, 353], [186, 344], [201, 343], [187, 335], [202, 334], [188, 325], [204, 325], [190, 315], [205, 315], [192, 305], [208, 303], [204, 296], [210, 288], [195, 288], [211, 278], [196, 278], [211, 268], [199, 268], [205, 259], [212, 250], [201, 242], [214, 237], [190, 234], [201, 230], [217, 229], [199, 220], [221, 220], [199, 211], [222, 210], [224, 201], [199, 200], [190, 189], [226, 190], [201, 190], [183, 181], [206, 181], [222, 180], [284, 173], [200, 173], [183, 172], [218, 172], [290, 163], [279, 164], [199, 164], [181, 161], [222, 161], [273, 155], [292, 155], [234, 153], [197, 153], [178, 152], [222, 147], [291, 145], [271, 144], [195, 143], [176, 142], [222, 138], [292, 135], [271, 136], [193, 133], [175, 133], [262, 129], [223, 128], [275, 125], [292, 125], [191, 123], [173, 122], [260, 119], [224, 118], [274, 115], [292, 114], [189, 113], [171, 112], [259, 110], [226, 108], [275, 105], [292, 105], [188, 103], [169, 102], [258, 99], [227, 98], [275, 94], [292, 95], [187, 94], [168, 92], [258, 89], [229, 88], [292, 85], [276, 84], [184, 82], [167, 82], [256, 79], [231, 77], [276, 76], [292, 76], [184, 73], [634, 74], [166, 72], [255, 69], [616, 67], [232, 68], [625, 68], [277, 64], [293, 66], [182, 63], [165, 62], [254, 60], [234, 58], [294, 55], [278, 55], [182, 54], [163, 52], [253, 48], [233, 48], [295, 45], [279, 45], [180, 43], [162, 42], [253, 40], [231, 38], [296, 34], [280, 34], [178, 33], [160, 32], [253, 28], [231, 28], [281, 26], [296, 25], [177, 23], [160, 23], [251, 19], [233, 18], [282, 15], [298, 15], [176, 13], [159, 13], [247, 9], [238, 9], [299, 5], [283, 5]], "IMG_02C458E4D7CE-5.jpg": [[189, 478], [262, 478], [273, 476], [555, 476], [199, 475], [535, 475], [599, 474], [283, 473], [216, 471], [454, 472], [522, 468], [225, 468], [292, 468], [548, 466], [273, 467], [595, 466], [235, 465], [301, 463], [245, 462], [449, 462], [259, 459], [313, 459], [523, 457], [542, 458], [612, 458], [270, 456], [296, 457], [591, 455], [331, 454], [280, 452], [444, 452], [357, 451], [339, 450], [536, 449], [321, 448], [581, 447], [518, 449], [299, 446], [309, 442], [347, 443], [440, 442], [592, 442], [531, 443], [576, 439], [502, 438], [324, 440], [517, 437], [354, 435], [334, 435], [593, 431], [437, 432], [367, 432], [572, 429], [511, 428], [377, 428], [391, 423], [593, 423], [505, 420], [570, 420], [404, 419], [416, 415], [374, 413], [497, 411], [432, 409], [569, 409], [441, 405], [489, 401], [450, 401], [568, 400], [460, 397], [429, 392], [484, 391], [469, 392], [563, 390], [497, 388], [523, 386], [426, 383], [509, 383], [556, 383], [538, 379], [546, 378], [475, 375], [517, 375], [425, 374], [501, 373], [469, 369], [423, 363], [462, 361], [425, 354], [454, 353], [448, 345], [429, 345], [441, 338], [436, 327], [431, 318], [5, 86], [15, 84], [24, 82], [36, 79], [46, 74], [55, 69], [64, 62], [26, 57], [75, 57], [13, 57], [83, 56], [61, 52], [94, 52], [104, 51], [177, 49], [121, 49], [142, 45], [51, 45], [155, 43], [130, 46], [171, 44], [29, 40], [41, 41], [114, 38], [150, 34], [71, 15], [58, 15], [84, 11], [48, 11], [635, 10], [100, 5], [147, 3], [40, 4]], "IMG_02C458E4D7CE-6.jpg": [[35, 479], [472, 479], [50, 478], [452, 477], [63, 476], [598, 474], [623, 475], [478, 470], [456, 469], [37, 469], [52, 467], [617, 467], [65, 466], [465, 461], [482, 461], [37, 458], [54, 458], [616, 457], [66, 457], [467, 453], [628, 451], [487, 453], [38, 449], [55, 448], [613, 448], [68, 446], [472, 444], [633, 444], [493, 442], [39, 439], [58, 438], [70, 436], [477, 435], [639, 435], [498, 434], [610, 433], [491, 435], [626, 431], [41, 429], [59, 429], [72, 427], [613, 426], [504, 426], [484, 424], [629, 421], [42, 419], [61, 418], [75, 416], [509, 416], [612, 415], [489, 413], [633, 412], [43, 409], [62, 408], [77, 406], [514, 406], [494, 406], [616, 404], [637, 402], [43, 399], [64, 398], [507, 398], [620, 396], [522, 393], [80, 391], [44, 388], [65, 388], [506, 386], [625, 386], [526, 385], [82, 383], [46, 379], [67, 378], [510, 378], [630, 376], [533, 376], [520, 376], [84, 373], [46, 370], [67, 367], [537, 368], [635, 366], [519, 363], [86, 363], [49, 359], [70, 357], [542, 357], [638, 356], [524, 355], [88, 353], [269, 348], [51, 349], [539, 346], [72, 348], [548, 347], [276, 341], [87, 344], [53, 339], [534, 338], [72, 338], [543, 339], [553, 338], [285, 336], [90, 334], [294, 331], [55, 328], [550, 328], [74, 328], [540, 329], [559, 327], [92, 324], [301, 324], [55, 320], [549, 318], [76, 319], [309, 319], [96, 315], [560, 311], [57, 309], [77, 308], [94, 306], [582, 305], [564, 303], [59, 299], [79, 299], [99, 297], [570, 294], [60, 290], [81, 288], [101, 287], [575, 285], [210, 278], [61, 278], [83, 278], [103, 277], [580, 275], [208, 270], [63, 269], [84, 269], [105, 267], [584, 267], [203, 259], [64, 259], [86, 257], [589, 258], [107, 257], [193, 253], [231, 252], [179, 249], [206, 250], [65, 249], [594, 249], [87, 248], [108, 247], [185, 244], [225, 245], [579, 244], [205, 243], [599, 240], [67, 239], [88, 239], [217, 238], [110, 237], [183, 237], [161, 233], [207, 229], [605, 230], [69, 228], [90, 228], [112, 226], [179, 227], [168, 223], [9, 221], [202, 221], [610, 221], [149, 218], [71, 219], [92, 219], [18, 217], [161, 218], [186, 216], [113, 216], [197, 212], [615, 212], [27, 212], [175, 209], [154, 211], [73, 209], [93, 208], [186, 207], [115, 206], [36, 206], [620, 203], [174, 201], [45, 199], [76, 199], [149, 199], [193, 199], [95, 197], [116, 196], [625, 194], [183, 194], [54, 194], [172, 191], [144, 189], [96, 188], [74, 187], [117, 186], [62, 188], [190, 185], [181, 183], [630, 184], [170, 181], [141, 180], [79, 177], [95, 177], [188, 176], [119, 175], [635, 175], [166, 171], [138, 171], [86, 169], [182, 167], [120, 165], [154, 162], [135, 159], [90, 159], [181, 157], [123, 156], [159, 153], [95, 149], [178, 148], [125, 145], [157, 141], [114, 142], [98, 141], [176, 137], [124, 135], [155, 132], [96, 129], [175, 127], [127, 125], [496, 121], [152, 122], [97, 120], [173, 117], [130, 115], [498, 114], [510, 113], [107, 111], [147, 111], [96, 110], [171, 107], [125, 106], [517, 107], [112, 105], [500, 105], [147, 102], [94, 100], [169, 97], [524, 96], [116, 94], [503, 94], [151, 92], [99, 91], [166, 87], [528, 87], [507, 85], [111, 84], [156, 82], [534, 78], [512, 75], [112, 74], [158, 73], [538, 69], [516, 65], [114, 64], [159, 62], [543, 58], [171, 54], [521, 55], [115, 54], [157, 52], [547, 48], [174, 48], [525, 46], [115, 44], [155, 41], [551, 38], [179, 36], [530, 36], [117, 34], [153, 32], [556, 29], [534, 27], [182, 27], [122, 25], [148, 23], [560, 19], [540, 17], [184, 17], [127, 16], [143, 15], [564, 10], [545, 7], [186, 6], [130, 6]], "IMG_02C458E4D7CE-7.jpg": [[326, 479], [295, 475], [318, 472], [307, 472], [287, 468], [166, 465], [308, 462], [278, 461], [151, 458], [162, 458], [142, 455], [299, 454], [268, 452], [134, 451], [151, 447], [292, 447], [121, 445], [260, 445], [112, 440], [97, 439], [285, 440], [88, 438], [252, 438], [77, 436], [32, 435], [66, 433], [22, 433], [47, 431], [275, 432], [12, 432], [244, 431], [56, 432], [227, 426], [182, 424], [267, 425], [39, 422], [239, 422], [23, 419], [176, 419], [258, 418], [11, 417], [228, 416], [169, 411], [248, 409], [124, 409], [219, 408], [100, 407], [90, 406], [75, 405], [113, 404], [159, 404], [240, 403], [143, 400], [210, 401], [57, 400], [66, 400], [20, 398], [47, 398], [33, 398], [131, 397], [11, 397], [88, 395], [233, 397], [112, 395], [202, 393], [190, 392], [213, 392], [103, 390], [224, 388], [67, 386], [49, 385], [77, 385], [93, 386], [188, 381], [199, 384], [40, 382], [216, 382], [30, 380], [10, 379], [20, 379], [181, 374], [207, 374], [197, 370], [164, 367], [174, 367], [197, 362], [184, 360], [164, 357], [152, 354], [130, 351], [181, 350], [124, 344], [174, 344], [155, 338], [11, 339], [22, 337], [116, 337], [38, 333], [106, 332], [148, 332], [49, 329], [116, 326], [99, 325], [59, 325], [140, 323], [69, 322], [79, 319], [632, 318], [87, 316], [133, 317], [99, 313], [625, 309], [123, 309], [144, 305], [106, 307], [619, 301], [152, 299], [136, 298], [574, 298], [107, 294], [611, 294], [567, 295], [160, 293], [556, 291], [637, 291], [544, 289], [169, 289], [150, 291], [100, 287], [180, 286], [604, 286], [200, 284], [533, 283], [629, 282], [190, 282], [211, 281], [222, 280], [93, 279], [522, 277], [597, 277], [553, 274], [230, 276], [199, 274], [106, 274], [624, 275], [86, 272], [517, 272], [590, 268], [238, 267], [505, 265], [550, 265], [616, 266], [76, 264], [248, 262], [515, 259], [583, 261], [494, 258], [257, 256], [69, 257], [48, 257], [608, 257], [549, 259], [618, 256], [269, 253], [502, 252], [576, 252], [521, 251], [61, 249], [278, 249], [601, 249], [492, 248], [288, 245], [569, 244], [594, 242], [53, 241], [485, 240], [310, 239], [507, 238], [297, 240], [68, 236], [319, 235], [561, 236], [634, 234], [45, 232], [587, 233], [480, 231], [502, 229], [310, 231], [554, 228], [626, 226], [37, 225], [349, 223], [579, 224], [472, 221], [495, 222], [357, 218], [548, 219], [30, 218], [619, 218], [572, 215], [366, 214], [557, 214], [466, 213], [488, 213], [376, 210], [20, 209], [542, 210], [609, 208], [564, 206], [634, 205], [390, 203], [459, 204], [481, 204], [470, 204], [12, 202], [536, 203], [375, 203], [396, 200], [547, 202], [604, 202], [558, 200], [627, 198], [450, 196], [509, 196], [406, 194], [418, 192], [473, 193], [597, 194], [550, 190], [459, 191], [619, 190], [444, 189], [425, 188], [505, 189], [529, 182], [610, 182], [11, 181], [468, 178], [437, 178], [592, 176], [525, 176], [476, 175], [639, 175], [574, 171], [459, 172], [429, 170], [445, 172], [510, 169], [485, 169], [497, 167], [632, 166], [568, 165], [452, 163], [422, 163], [471, 160], [587, 158], [480, 159], [627, 158], [563, 159], [416, 155], [446, 155], [495, 150], [582, 151], [475, 150], [620, 148], [438, 147], [408, 147], [559, 142], [512, 141], [576, 145], [471, 139], [614, 141], [402, 139], [431, 139], [481, 140], [529, 138], [553, 136], [639, 136], [501, 134], [464, 132], [489, 134], [424, 131], [393, 130], [511, 130], [522, 129], [545, 128], [631, 126], [456, 124], [388, 124], [415, 121], [491, 121], [432, 118], [537, 119], [558, 119], [612, 118], [449, 116], [380, 114], [409, 114], [484, 113], [564, 114], [529, 111], [607, 113], [426, 111], [441, 108], [400, 105], [372, 106], [494, 107], [573, 105], [477, 105], [522, 104], [599, 103], [433, 100], [235, 96], [365, 99], [394, 98], [470, 97], [514, 97], [569, 97], [597, 91], [425, 91], [495, 90], [192, 89], [358, 89], [385, 90], [230, 88], [463, 90], [562, 88], [612, 86], [490, 85], [418, 85], [595, 82], [352, 82], [378, 82], [185, 81], [222, 81], [617, 82], [626, 78], [509, 79], [209, 80], [457, 80], [556, 79], [400, 77], [410, 75], [345, 74], [371, 74], [590, 74], [178, 74], [360, 75], [504, 72], [449, 71], [213, 72], [550, 70], [403, 67], [169, 66], [338, 66], [585, 65], [441, 64], [481, 62], [499, 66], [205, 63], [361, 61], [543, 62], [162, 59], [396, 59], [428, 55], [475, 57], [196, 56], [581, 56], [535, 54], [354, 52], [389, 51], [156, 51], [468, 49], [188, 49], [427, 49], [175, 46], [332, 45], [575, 47], [348, 47], [528, 44], [381, 43], [149, 43], [461, 41], [374, 38], [420, 40], [326, 39], [567, 38], [523, 38], [141, 35], [319, 31], [415, 34], [455, 30], [637, 30], [369, 30], [562, 30], [516, 29], [131, 27], [449, 24], [631, 22], [313, 25], [122, 21], [556, 20], [442, 15], [114, 13], [633, 13], [548, 12], [432, 9], [415, 4], [106, 6], [539, 6]], "IMG_02C458E4D7CE-9.jpg": [[539, 479], [497, 478], [547, 475], [514, 472], [579, 468], [526, 468], [502, 467], [567, 464], [534, 464], [553, 461], [580, 459], [592, 455], [561, 452], [615, 449], [546, 450], [601, 451], [586, 449], [569, 447], [624, 443], [610, 438], [635, 438], [588, 438], [598, 434], [608, 430], [618, 424], [627, 422], [635, 416], [116, 353], [126, 344], [120, 343], [121, 334], [111, 324], [122, 322], [165, 319], [111, 316], [163, 310], [132, 310], [119, 309], [159, 300], [121, 298], [155, 292], [118, 289], [127, 285], [149, 280], [112, 281], [135, 278], [108, 271], [151, 270], [135, 264], [106, 261], [131, 256], [104, 251], [93, 249], [128, 246], [100, 241], [125, 236], [357, 232], [98, 231], [124, 226], [362, 225], [79, 224], [630, 223], [94, 222], [610, 218], [620, 220], [370, 218], [124, 217], [599, 214], [85, 214], [379, 213], [97, 211], [572, 208], [586, 210], [389, 207], [123, 206], [79, 205], [398, 203], [636, 202], [554, 202], [563, 203], [98, 200], [408, 200], [422, 198], [537, 197], [360, 198], [615, 196], [123, 197], [625, 197], [74, 195], [432, 194], [527, 193], [605, 192], [96, 192], [366, 191], [443, 191], [512, 188], [589, 188], [69, 186], [121, 186], [579, 185], [523, 186], [452, 183], [373, 183], [506, 185], [567, 181], [95, 182], [461, 181], [470, 180], [557, 178], [481, 178], [65, 177], [119, 176], [530, 174], [494, 173], [381, 174], [446, 174], [546, 173], [92, 172], [501, 172], [517, 170], [438, 168], [455, 168], [59, 167], [118, 166], [388, 166], [553, 164], [378, 165], [488, 164], [90, 161], [479, 159], [431, 158], [395, 158], [54, 156], [422, 157], [407, 156], [117, 156], [445, 154], [468, 155], [455, 153], [69, 151], [87, 151], [390, 152], [49, 147], [77, 145], [116, 146], [90, 140], [72, 137], [45, 138], [115, 136], [91, 130], [66, 127], [40, 126], [114, 127], [90, 120], [62, 118], [112, 116], [34, 116], [90, 110], [56, 108], [30, 107], [110, 106], [88, 99], [51, 98], [110, 96], [26, 97], [87, 90], [46, 88], [20, 87], [109, 85], [86, 79], [41, 79], [16, 77], [108, 75], [86, 70], [12, 67], [35, 65], [107, 66], [630, 64], [619, 62], [85, 60], [600, 59], [9, 61], [589, 57], [31, 56], [576, 55], [106, 55], [562, 51], [85, 51], [549, 48], [25, 48], [537, 45], [106, 46], [5, 41], [83, 40], [105, 35], [83, 30], [104, 25], [84, 18], [100, 15], [88, 10]]} \ No newline at end of file diff --git a/p_algorithm_precision.py b/p_algorithm_precision.py new file mode 100644 index 0000000000000000000000000000000000000000..b8dc8abdc552b3b517febf284510427d3b79cb18 --- /dev/null +++ b/p_algorithm_precision.py @@ -0,0 +1,898 @@ +#!/usr/bin/env python3 +""" +Complete Hair Counting Pipeline: +- Steps 1-7: BSR, Preprocess, Binarize, Thinning, MSLD, PLB, Merge +- Step 8: Relaxation Labeling for clustering line segments +- Step 9: Count hairs +- Visualization +""" + +import os +import cv2 +import numpy as np +from skimage.morphology import skeletonize +import math +from tqdm import tqdm +import glob +from collections import defaultdict + +# ----------------------------- Utilities ----------------------------------- +def ensure_dir(p): + os.makedirs(p, exist_ok=True) + +# ----------------------------- BSR module ---------------------------------- +def bsr_lab_opening(rgb, se_radius=6): + """ + Bright Spot Removal via morphological opening on L channel in LAB color-space. + """ + lab = cv2.cvtColor(rgb, cv2.COLOR_BGR2LAB) + L, A, B = cv2.split(lab) + k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2*se_radius+1, 2*se_radius+1)) + L_open = cv2.morphologyEx(L, cv2.MORPH_OPEN, k) + L2 = cv2.normalize(L - (L - L_open)//1, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) + lab2 = cv2.merge([L2, A, B]) + out = cv2.cvtColor(lab2, cv2.COLOR_LAB2BGR) + return out + +# ----------------------------- Preprocessing -------------------------------- +def preprocess(rgb, bilateral_d=9, bilateral_sigmaColor=75, bilateral_sigmaSpace=75): + b = cv2.bilateralFilter(rgb, bilateral_d, bilateral_sigmaColor, bilateral_sigmaSpace) + return b + +# ----------------------------- Binarization --------------------------------- +def binarize(img_gray, morph_radius=3): + blur = cv2.GaussianBlur(img_gray, (5,5), 0) + _, th = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) + k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2*morph_radius+1, 2*morph_radius+1)) + th = cv2.morphologyEx(th, cv2.MORPH_CLOSE, k, iterations=1) + th = cv2.morphologyEx(th, cv2.MORPH_OPEN, k, iterations=1) + return th + +# ----------------------------- Thinning ------------------------------------ +def thin_mask(binary_u8): + bw = (binary_u8 > 0) + skel = skeletonize(bw) + return (skel.astype(np.uint8) * 255) + +# ----------------------------- MSLD (multi-scale Hough) -------------------- +def multi_scale_hough(edges, scale_factors=[1.0, 0.75, 0.5], hough_params=None): + """ + Run Probabilistic HoughLinesP at multiple scales. + """ + if hough_params is None: + hough_params = {'rho':1, 'theta':np.pi/180, 'threshold':30, 'minLineLength':20, 'maxLineGap':20} + lines_all = [] + h, w = edges.shape + for s in scale_factors: + if s != 1.0: + small = cv2.resize(edges, (int(w*s), int(h*s)), interpolation=cv2.INTER_LINEAR) + else: + small = edges + lines = cv2.HoughLinesP(small, hough_params['rho'], hough_params['theta'], + hough_params['threshold'], + minLineLength=max(8, int(hough_params['minLineLength']*s)), + maxLineGap=max(1, int(hough_params['maxLineGap']*s))) + if lines is None: + continue + for l in lines: + x1,y1,x2,y2 = l[0] + if s != 1.0: + x1 = int(round(x1 / s)); y1 = int(round(y1 / s)) + x2 = int(round(x2 / s)); y2 = int(round(y2 / s)) + lines_all.append((x1,y1,x2,y2)) + + # Deduplicate + unique = [] + def close(a,b, tol=6): + return abs(a[0]-b[0])<=tol and abs(a[1]-b[1])<=tol and abs(a[2]-b[2])<=tol and abs(a[3]-b[3])<=tol + for l in lines_all: + if not any(close(l, u) or close(l, u[::-1]) for u in unique): + unique.append(l) + return unique + +# ----------------------------- PLB: Parallel Line Bundling ------------------- +def line_to_abcline(line): + x1,y1,x2,y2 = line + dx = x2 - x1; dy = y2 - y1 + if dx==0 and dy==0: + return None + a = dy; b = -dx + norm = math.hypot(a,b) + a /= norm; b /= norm + c = -(a*x1 + b*y1) + return (a,b,c) + +def line_angle(line): + x1,y1,x2,y2 = line + ang = math.atan2(y2-y1, x2-x1) + return ang + +def distance_between_parallel_lines(l1_abc, l2_abc): + a1,b1,c1 = l1_abc; a2,b2,c2 = l2_abc + return abs(c1 - c2) + +def seg_projection_on_line(seg, line_dir): + x1,y1,x2,y2 = seg + vx = math.cos(line_dir); vy = math.sin(line_dir) + p1 = x1*vx + y1*vy + p2 = x2*vx + y2*vy + return min(p1,p2), max(p1,p2) + +def overlap_segment_length(a1,b1,a2,b2): + left = max(a1,a2); right = min(b1,b2) + return max(0.0, right-left) + +def plb_restore(lines, avg_gap=None, gap_thresh_factor=1.15, angle_tol_deg=6, min_overlap_px=10): + """ + Parallel Line Bundling to restore concealed hairs + """ + out_lines = list(lines) + if len(lines) < 2: + return out_lines + + gaps = [] + abc_list = [] + for l in lines: + abc = line_to_abcline(l) + if abc is None: abc_list.append(None) + else: abc_list.append(abc) + + for i in range(len(lines)): + for j in range(i+1, len(lines)): + if abc_list[i] is None or abc_list[j] is None: continue + ang_i = line_angle(lines[i]); ang_j = line_angle(lines[j]) + if abs((ang_i-ang_j)+math.pi) < 0.001: ang_j += math.pi + angdiff = abs((ang_i - ang_j)) + angdiff = min(angdiff, abs(2*math.pi - angdiff)) + if angdiff > math.radians(angle_tol_deg): + continue + d = distance_between_parallel_lines(abc_list[i], abc_list[j]) + if d <= 0.5: + continue + gaps.append(d) + + if len(gaps)>0: + if avg_gap is None: + avg_gap = np.median(gaps) + else: + avg_gap = avg_gap or 8.0 + + # Pairwise restore + for i in range(len(lines)): + for j in range(i+1, len(lines)): + if abc_list[i] is None or abc_list[j] is None: continue + ang_i = line_angle(lines[i]); ang_j = line_angle(lines[j]) + angdiff = abs((ang_i - ang_j)) + angdiff = min(angdiff, abs(2*math.pi - angdiff)) + if angdiff > math.radians(angle_tol_deg): + continue + d = distance_between_parallel_lines(abc_list[i], abc_list[j]) + if d < avg_gap * gap_thresh_factor * 0.7 or d > avg_gap * gap_thresh_factor * 2.5: + continue + + dir_ang = 0.5*(ang_i + ang_j) + a1,b1 = seg_projection_on_line(lines[i], dir_ang) + a2,b2 = seg_projection_on_line(lines[j], dir_ang) + ov = overlap_segment_length(a1,b1,a2,b2) + if ov < min_overlap_px: + continue + + mid_start = (max(a1,a2)) + mid_end = (min(b1,b2)) + + def point_on_seg_by_proj(seg, proj_val, dir_ang): + x1,y1,x2,y2 = seg + vx = math.cos(dir_ang); vy = math.sin(dir_ang) + p1 = x1*vx + y1*vy; p2 = x2*vx + y2*vy + if p2==p1: + alpha = 0.0 + else: + alpha = (proj_val - p1) / (p2 - p1) + alpha = max(0.0, min(1.0, alpha)) + return (int(round(x1 + alpha * (x2-x1))), int(round(y1 + alpha * (y2-y1)))) + + p_start_i = point_on_seg_by_proj(lines[i], mid_start, dir_ang) + p_start_j = point_on_seg_by_proj(lines[j], mid_start, dir_ang) + p_end_i = point_on_seg_by_proj(lines[i], mid_end, dir_ang) + p_end_j = point_on_seg_by_proj(lines[j], mid_end, dir_ang) + + mid_start_pt = (int(round(0.5*(p_start_i[0]+p_start_j[0]))), int(round(0.5*(p_start_i[1]+p_start_j[1])))) + mid_end_pt = (int(round(0.5*(p_end_i[0]+p_end_j[0]))), int(round(0.5*(p_end_i[1]+p_end_j[1])))) + + out_lines.append((mid_start_pt[0], mid_start_pt[1], mid_end_pt[0], mid_end_pt[1])) + + return out_lines + +# ----------------------------- lines -> mask -------------------------------- +def rasterize_lines_to_mask(lines, shape, thickness=3): + mask = np.zeros(shape[:2], dtype=np.uint8) + for (x1,y1,x2,y2) in lines: + cv2.line(mask, (x1,y1), (x2,y2), color=255, thickness=thickness) + mask = cv2.dilate(mask, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3)), iterations=1) + return mask +# ----------------------------- Relaxation Labeling (IMPROVED) --------------- + +def line_to_polar(line): + """ + Convert line segment to polar coordinates (rho, theta) + """ + x1, y1, x2, y2 = line + + # Compute angle (orientation) - normalize to [0, pi] + theta = math.atan2(y2 - y1, x2 - x1) + if theta < 0: + theta += math.pi + + # Compute rho (perpendicular distance from origin to line) + # Using the standard Hesse normal form + if abs(x2 - x1) < 1e-6 and abs(y2 - y1) < 1e-6: + # Degenerate case + rho = math.hypot(x1, y1) + else: + # Distance from origin (0,0) to the infinite line passing through the segment + rho = abs((y2-y1)*0 - (x2-x1)*0 + x2*y1 - y2*x1) / math.hypot(x2-x1, y2-y1) + + return rho, theta + +def get_line_midpoint(line): + """Get midpoint of line segment""" + x1, y1, x2, y2 = line + return ((x1 + x2) / 2.0, (y1 + y2) / 2.0) + +def get_line_length(line): + """Get length of line segment""" + x1, y1, x2, y2 = line + return math.hypot(x2 - x1, y2 - y1) + +def distance_between_points(p1, p2): + """Euclidean distance between two points""" + return math.hypot(p1[0] - p2[0], p1[1] - p2[1]) + +def point_to_line_distance(point, line): + """ + Minimum distance from a point to a line segment + """ + x1, y1, x2, y2 = line + px, py = point + + # Vector from line start to end + dx = x2 - x1 + dy = y2 - y1 + + if dx == 0 and dy == 0: + # Degenerate line segment + return math.hypot(px - x1, py - y1) + + # Parameter t for projection of point onto line + t = max(0, min(1, ((px - x1) * dx + (py - y1) * dy) / (dx * dx + dy * dy))) + + # Closest point on line segment + closest_x = x1 + t * dx + closest_y = y1 + t * dy + + return math.hypot(px - closest_x, py - closest_y) + +def line_to_line_distance(line1, line2): + """ + Minimum distance between two line segments + """ + # Check distance from endpoints of line1 to line2 + x1, y1, x2, y2 = line1 + d1 = point_to_line_distance((x1, y1), line2) + d2 = point_to_line_distance((x2, y2), line2) + + # Check distance from endpoints of line2 to line1 + x1, y1, x2, y2 = line2 + d3 = point_to_line_distance((x1, y1), line1) + d4 = point_to_line_distance((x2, y2), line1) + + return min(d1, d2, d3, d4) + +def find_neighbors_improved(lines, max_distance=100, max_angle_diff_deg=30): + """ + Find neighboring line segments with improved criteria: + - Close in space (line-to-line distance) + - Similar orientation + """ + n = len(lines) + neighbors = [set() for _ in range(n)] + + # Precompute angles + angles = [line_angle(line) for line in lines] + + for i in range(n): + for j in range(i + 1, n): + # Check angle similarity + angle_i = angles[i] + angle_j = angles[j] + + # Normalize angle difference to [0, pi] + angle_diff = abs(angle_i - angle_j) + if angle_diff > math.pi: + angle_diff = 2 * math.pi - angle_diff + # Also check if they're opposite directions (should still be grouped) + angle_diff = min(angle_diff, math.pi - angle_diff) + + if angle_diff > math.radians(max_angle_diff_deg): + continue + + # Check spatial proximity (line-to-line distance) + dist = line_to_line_distance(lines[i], lines[j]) + + if dist <= max_distance: + neighbors[i].add(j) + neighbors[j].add(i) + + return neighbors + +def agglomerative_clustering(lines, max_cluster_distance=80, max_angle_diff_deg=25): + """ + Simple agglomerative clustering based on: + - Lines that are close in space + - Lines that have similar orientation + + This is more robust than Relaxation Labeling for this problem. + """ + n = len(lines) + if n == 0: + return [], 0 + + # Initialize: each line is its own cluster + clusters = [[i] for i in range(n)] + + # Precompute angles + angles = [line_angle(line) for line in lines] + + def cluster_angle(cluster_indices): + """Average angle of lines in a cluster""" + cluster_angles = [angles[i] for i in cluster_indices] + # Use circular mean for angles + x = sum(math.cos(a) for a in cluster_angles) + y = sum(math.sin(a) for a in cluster_angles) + return math.atan2(y, x) + + def cluster_center(cluster_indices): + """Center point of all line midpoints in cluster""" + midpoints = [get_line_midpoint(lines[i]) for i in cluster_indices] + cx = sum(p[0] for p in midpoints) / len(midpoints) + cy = sum(p[1] for p in midpoints) / len(midpoints) + return (cx, cy) + + def cluster_distance(c1, c2): + """ + Distance between two clusters based on: + - Spatial distance between centers + - Angle difference + """ + center1 = cluster_center(c1) + center2 = cluster_center(c2) + spatial_dist = distance_between_points(center1, center2) + + angle1 = cluster_angle(c1) + angle2 = cluster_angle(c2) + angle_diff = abs(angle1 - angle2) + angle_diff = min(angle_diff, 2 * math.pi - angle_diff, math.pi - angle_diff) + + # Combined metric: spatial distance + angle penalty + if angle_diff > math.radians(max_angle_diff_deg): + return float('inf') # Don't merge if angles too different + + return spatial_dist + + # Agglomerative merging + changed = True + while changed and len(clusters) > 1: + changed = False + best_merge = None + best_dist = max_cluster_distance + + # Find best pair to merge + for i in range(len(clusters)): + for j in range(i + 1, len(clusters)): + dist = cluster_distance(clusters[i], clusters[j]) + if dist < best_dist: + best_dist = dist + best_merge = (i, j) + changed = True + + # Merge best pair + if best_merge: + i, j = best_merge + clusters[i].extend(clusters[j]) + del clusters[j] + + # Assign labels + labels = [-1] * n + for cluster_id, cluster in enumerate(clusters): + for line_idx in cluster: + labels[line_idx] = cluster_id + + return labels, len(clusters) + +def relaxation_labeling_improved(lines, max_iterations=30, epsilon=0.7, + max_neighbor_dist=100, max_angle_diff_deg=25, + convergence_threshold=0.001): + """ + Improved Relaxation Labeling with better parameters and compatibility function + """ + n = len(lines) + if n == 0: + return [], 0 + + # Use improved neighbor finding + neighbors = find_neighbors_improved(lines, max_distance=max_neighbor_dist, + max_angle_diff_deg=max_angle_diff_deg) + + # Precompute angles + angles = [line_angle(line) for line in lines] + + # Precompute midpoints + midpoints = [get_line_midpoint(line) for line in lines] + + # Initialize: Start with connected components as initial labels + # This gives a better initialization than one-label-per-line + visited = [False] * n + initial_labels = [-1] * n + current_label = 0 + + for start in range(n): + if visited[start]: + continue + + # BFS to find connected component + queue = [start] + visited[start] = True + + while queue: + i = queue.pop(0) + initial_labels[i] = current_label + + for j in neighbors[i]: + if not visited[j]: + visited[j] = True + queue.append(j) + + current_label += 1 + + num_labels = current_label + + if num_labels == 0: + # No neighbors found, each line is separate + return list(range(n)), n + + # Initialize probability matrix + p = np.zeros((n, num_labels), dtype=np.float64) + for i in range(n): + if initial_labels[i] >= 0: + p[i, initial_labels[i]] = 1.0 + else: + p[i, :] = 1.0 / num_labels + + # Iterative relaxation + for iteration in range(max_iterations): + q = np.zeros((n, num_labels), dtype=np.float64) + + for i in range(n): + for label_i in range(num_labels): + support = 0.0 + + for j in neighbors[i]: + # Compute compatibility based on angle similarity + angle_diff = abs(angles[i] - angles[j]) + angle_diff = min(angle_diff, 2 * math.pi - angle_diff, math.pi - angle_diff) + angle_sim = math.cos(angle_diff) # 1 if same, 0 if perpendicular + + # Distance similarity + dist = distance_between_points(midpoints[i], midpoints[j]) + dist_sim = math.exp(-dist / max_neighbor_dist) # Exponential decay + + # Combined compatibility + compatibility = epsilon * angle_sim + (1 - epsilon) * dist_sim + + # Accumulate support for same label + support += compatibility * p[j, label_i] + + q[i, label_i] = support + + # Update probabilities + p_new = np.zeros_like(p) + for i in range(n): + for label in range(num_labels): + p_new[i, label] = p[i, label] * (1 + q[i, label]) + + # Normalize + row_sum = np.sum(p_new[i, :]) + if row_sum > 1e-10: + p_new[i, :] /= row_sum + else: + p_new[i, :] = p[i, :] + + # Check convergence + diff = np.abs(p_new - p).max() + p = p_new + + if diff < convergence_threshold: + break + + # Assign final labels + final_labels = np.argmax(p, axis=1) + + # Renumber to consecutive + unique_labels = np.unique(final_labels) + label_mapping = {old: new for new, old in enumerate(unique_labels)} + final_labels = np.array([label_mapping[label] for label in final_labels]) + + return final_labels.tolist(), len(unique_labels) + +# ----------------------------- Update main pipeline to use improved version --- +def visualize_labeled_lines(rgb, lines, labels, title="Labeled Hairs"): + """ + Visualize line segments colored by their labels (hair clusters) + """ + vis = rgb.copy() + + # Generate colors for each unique label + unique_labels = sorted(set(labels)) + num_labels = len(unique_labels) + + # Create colormap + np.random.seed(42) + colors = [] + for i in range(num_labels): + colors.append(( + np.random.randint(50, 255), + np.random.randint(50, 255), + np.random.randint(50, 255) + )) + + # Draw each line with its label color + label_to_color = {label: colors[i] for i, label in enumerate(unique_labels)} + + for line, label in zip(lines, labels): + x1, y1, x2, y2 = line + color = label_to_color[label] + cv2.line(vis, (x1, y1), (x2, y2), color, 2) + + # Add title + cv2.putText(vis, title, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, + 1.0, (255, 255, 255), 3) + cv2.putText(vis, title, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, + 1.0, (0, 0, 0), 1) + + return vis + +def run_complete_pipeline(image_path, out_dir, params, verbose=True): + imname = os.path.basename(image_path) + rgb = cv2.imread(image_path) + if rgb is None: + raise RuntimeError("Cannot open image: " + image_path) + + if verbose: + print(f"\nProcessing: {imname}") + print("="*60) + print(" Step 1: BSR (Bright Spot Removal)...") + bsr = bsr_lab_opening(rgb, se_radius=params.get('bsr_se', 6)) + + if verbose: + print(" Step 2: Preprocessing (Bilateral Filter)...") + prep = preprocess(bsr, + bilateral_d=params.get('bilateral_d',9), + bilateral_sigmaColor=params.get('bilateral_sigmaColor',75), + bilateral_sigmaSpace=params.get('bilateral_sigmaSpace',75)) + + if verbose: + print(" Step 3: Binarization (Otsu + Morphology)...") + gray = cv2.cvtColor(prep, cv2.COLOR_BGR2GRAY) + binary = binarize(gray, morph_radius=params.get('morph_radius',3)) + + if verbose: + print(" Step 4: Thinning (Skeletonize)...") + skel = thin_mask(binary) + + if verbose: + print(" Step 5: MSLD (Multi-Scale Line Detection)...") + edges = cv2.Canny(gray, 50, 150) + lines = multi_scale_hough(edges, scale_factors=params.get('scales',[1.0,0.75,0.5]), + hough_params=params.get('hough_params', None)) + if verbose: + print(f" - Detected {len(lines)} lines") + + if verbose: + print(" Step 6: PLB (Parallel Line Bundling)...") + restored_lines = plb_restore(lines, avg_gap=params.get('avg_gap', None), + gap_thresh_factor=params.get('gap_factor', 1.25), + angle_tol_deg=params.get('angle_tol_deg', 6), + min_overlap_px=params.get('min_overlap_px', 12)) + if verbose: + print(f" - Restored to {len(restored_lines)} lines") + print(f" - Concealed hairs recovered: {len(restored_lines)-len(lines)}") + + if verbose: + print(" Step 7: Merge lines mask with binary...") + lines_mask = rasterize_lines_to_mask(restored_lines, rgb.shape, thickness=params.get('line_thickness',3)) + merged_foreground = cv2.bitwise_or(binary, lines_mask) + + if verbose: + print(" Step 8: Clustering line segments into hairs...") + + # Choose clustering method + clustering_method = params.get('clustering_method', 'agglomerative') # 'agglomerative' or 'relaxation' + + if clustering_method == 'agglomerative': + if verbose: + print(" - Using Agglomerative Clustering...") + labels, num_hairs = agglomerative_clustering( + restored_lines, + max_cluster_distance=params.get('cluster_max_dist', 80), + max_angle_diff_deg=params.get('cluster_angle_diff', 25) + ) + else: + if verbose: + print(" - Using Relaxation Labeling...") + labels, num_hairs = relaxation_labeling_improved( + restored_lines, + max_iterations=params.get('rl_max_iter', 30), + epsilon=params.get('rl_epsilon', 0.7), + max_neighbor_dist=params.get('rl_neighbor_dist', 100), + max_angle_diff_deg=params.get('rl_angle_diff', 25), + convergence_threshold=params.get('rl_conv_threshold', 0.001) + ) + + if verbose: + print(f" - Clustered into {num_hairs} hairs") + print("="*60) + + # ========== VISUALIZATION ========== (keep same as before) + if verbose: + print(" Creating visualizations...") + + lines_vis = rgb.copy() + for (x1,y1,x2,y2) in lines: + cv2.line(lines_vis, (x1,y1), (x2,y2), (0,255,0), 2) + + restored_vis = rgb.copy() + for (x1,y1,x2,y2) in restored_lines: + cv2.line(restored_vis, (x1,y1), (x2,y2), (0,255,255), 2) + + labeled_vis = visualize_labeled_lines(rgb, restored_lines, labels, + f"Hairs: {num_hairs}") + + binary_bgr = cv2.cvtColor(binary, cv2.COLOR_GRAY2BGR) + skel_bgr = cv2.cvtColor(skel, cv2.COLOR_GRAY2BGR) + lines_mask_bgr = cv2.cvtColor(lines_mask, cv2.COLOR_GRAY2BGR) + merged_bgr = cv2.cvtColor(merged_foreground, cv2.COLOR_GRAY2BGR) + edges_bgr = cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR) + + target_size = (512, 512) + def resize_and_label(img, text): + resized = cv2.resize(img, target_size) + cv2.putText(resized, text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, + 0.7, (255, 255, 255), 2) + cv2.putText(resized, text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, + 0.7, (0, 0, 0), 1) + return resized + + rgb_res = resize_and_label(rgb, "1. Original") + bsr_res = resize_and_label(bsr, "2. BSR") + prep_res = resize_and_label(prep, "3. Preprocessed") + binary_res = resize_and_label(binary_bgr, "4. Binary") + skel_res = resize_and_label(skel_bgr, "5. Skeleton") + edges_res = resize_and_label(edges_bgr, "6. Edges") + lines_vis_res = resize_and_label(lines_vis, f"7. Lines ({len(lines)})") + restored_vis_res = resize_and_label(restored_vis, f"8. PLB ({len(restored_lines)})") + lines_mask_res = resize_and_label(lines_mask_bgr, "9. Lines Mask") + merged_res = resize_and_label(merged_bgr, "10. Merged") + labeled_res = resize_and_label(labeled_vis, f"11. Labeled ({num_hairs})") + + count_img = np.zeros((target_size[1], target_size[0], 3), dtype=np.uint8) + count_img[:] = (40, 40, 40) + + method_name = "Agglomerative" if clustering_method == 'agglomerative' else "Relaxation" + + info_text = [ + f"Image: {imname}", + "", + f"Lines detected: {len(lines)}", + f"After PLB: {len(restored_lines)}", + f"Recovered: +{len(restored_lines)-len(lines)}", + "", + f"Hair Count: {num_hairs}", + "", + f"Method: {method_name}", + f"Max distance: {params.get('cluster_max_dist', 80)}", + f"Max angle diff: {params.get('cluster_angle_diff', 25)}°" + ] + + y_offset = 50 + for text in info_text: + cv2.putText(count_img, text, (20, y_offset), + cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1) + y_offset += 35 + + row1 = np.hstack([rgb_res, bsr_res, prep_res, binary_res]) + row2 = np.hstack([skel_res, edges_res, lines_vis_res, restored_vis_res]) + row3 = np.hstack([lines_mask_res, merged_res, labeled_res, count_img]) + combined = np.vstack([row1, row2, row3]) + + out_vis_path = os.path.join(out_dir, "complete_pipeline_" + imname) + cv2.imwrite(out_vis_path, combined) + + labeled_path = os.path.join(out_dir, "labeled_" + imname) + cv2.imwrite(labeled_path, labeled_vis) + + cv2.imwrite(os.path.join(out_dir, "binary_" + imname), binary) + cv2.imwrite(os.path.join(out_dir, "lines_mask_" + imname), lines_mask) + cv2.imwrite(os.path.join(out_dir, "merged_foreground_" + imname), merged_foreground) + + if verbose: + print(f" ✓ Visualization saved: {out_vis_path}") + print(f" ✓ Labeled image saved: {labeled_path}") + + return { + 'image': image_path, + 'original': rgb, + 'lines': lines, + 'restored_lines': restored_lines, + 'labels': labels, + 'num_hairs': num_hairs, + 'binary': binary, + 'merged_foreground': merged_foreground, + 'vis_path': out_vis_path, + 'labeled_path': labeled_path + } +# ----------------------------- Batch processing -------------------------------- +def process_folder(input_folder, output_folder, params): + """ + Process all images in a folder + """ + ensure_dir(output_folder) + + # Find all image files + image_extensions = ['*.jpg', '*.jpeg', '*.png', '*.bmp', '*.tiff', '*.tif'] + image_files = [] + for ext in image_extensions: + image_files.extend(glob.glob(os.path.join(input_folder, ext))) + image_files.extend(glob.glob(os.path.join(input_folder, ext.upper()))) + + image_files = sorted(list(set(image_files))) # Remove duplicates and sort + + if len(image_files) == 0: + print(f"No images found in {input_folder}") + return [], [] + + print(f"Found {len(image_files)} images in {input_folder}") + print(f"Output will be saved to: {output_folder}\n") + + results = [] + failed = [] + + # Process each image with progress bar + for img_path in tqdm(image_files, desc="Processing images"): + try: + result = run_complete_pipeline(img_path, output_folder, params, verbose=False) + results.append(result) + print(f"✓ {os.path.basename(img_path)}: " + f"{len(result['lines'])} → {len(result['restored_lines'])} lines → " + f"{result['num_hairs']} hairs") + except Exception as e: + failed.append((img_path, str(e))) + print(f"✗ {os.path.basename(img_path)}: ERROR - {str(e)}") + + # Summary statistics + print("\n" + "="*80) + print(f"SUMMARY:") + print(f" Total images: {len(image_files)}") + print(f" Successfully processed: {len(results)}") + print(f" Failed: {len(failed)}") + + if len(results) > 0: + total_original = sum(len(r['lines']) for r in results) + total_restored = sum(len(r['restored_lines']) for r in results) + total_hairs = sum(r['num_hairs'] for r in results) + + print(f"\n Total lines detected: {total_original}") + print(f" Total after PLB restoration: {total_restored}") + print(f" Total concealed hairs recovered: {total_restored - total_original}") + print(f"\n TOTAL HAIR COUNT: {total_hairs}") + print(f" Average hairs per image: {total_hairs / len(results):.1f}") + + # Hair count distribution + hair_counts = [r['num_hairs'] for r in results] + print(f"\n Hair count statistics:") + print(f" Min: {min(hair_counts)}") + print(f" Max: {max(hair_counts)}") + print(f" Mean: {np.mean(hair_counts):.1f}") + print(f" Median: {np.median(hair_counts):.1f}") + print(f" Std: {np.std(hair_counts):.1f}") + + if len(failed) > 0: + print(f"\n Failed images:") + for path, error in failed: + print(f" - {os.path.basename(path)}: {error}") + + print("="*80) + + # Save summary to CSV + if len(results) > 0: + import csv + csv_path = os.path.join(output_folder, "hair_count_summary.csv") + with open(csv_path, 'w', newline='') as f: + writer = csv.writer(f) + writer.writerow(['Image', 'Lines_Detected', 'Lines_After_PLB', + 'Lines_Recovered', 'Hair_Count']) + for r in results: + writer.writerow([ + os.path.basename(r['image']), + len(r['lines']), + len(r['restored_lines']), + len(r['restored_lines']) - len(r['lines']), + r['num_hairs'] + ]) + print(f"\n✓ Summary saved to: {csv_path}") + + return results, failed + +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser(description='Complete Hair Counting Pipeline') + parser.add_argument('--image', type=str, required=False) + parser.add_argument('--folder', type=str, default="/Users/Admin/ScalpVision/datasets/data") + parser.add_argument('--out', type=str, default="./complete_pipeline_out") + + # Clustering method + parser.add_argument('--method', type=str, default='agglomerative', + choices=['agglomerative', 'relaxation'], + help="Clustering method: agglomerative (recommended) or relaxation") + + # Clustering parameters + parser.add_argument('--cluster-dist', type=float, default=200, + help="Max distance for clustering (recommended: 60-100)") + parser.add_argument('--cluster-angle', type=float, default=30, + help="Max angle difference in degrees (recommended: 20-30)") + + args = parser.parse_args() + + ensure_dir(args.out) + + params = { + 'bsr_se': 5, + 'bilateral_d': 9, + 'bilateral_sigmaColor': 75, + 'bilateral_sigmaSpace': 75, + 'morph_radius': 3, + 'scales': [1.0, 0.75, 0.5], + 'hough_params': { + 'rho': 1, + 'theta': np.pi/180, + 'threshold': 33, + 'minLineLength': 30, + 'maxLineGap': 20 + }, + 'avg_gap': None, + 'gap_factor': 1.25, + 'angle_tol_deg': 6, + 'min_overlap_px': 12, + 'line_thickness': 3, + + # Clustering parameters + 'clustering_method': args.method, + 'cluster_max_dist': args.cluster_dist, + 'cluster_angle_diff': args.cluster_angle, + + # Relaxation Labeling (if used) + 'rl_max_iter': 30, + 'rl_epsilon': 0.7, + 'rl_neighbor_dist': 100, + 'rl_angle_diff': 25, + 'rl_conv_threshold': 0.001 + } + + if args.image: + result = run_complete_pipeline(args.image, args.out, params, verbose=True) + print(f"\n{'='*60}") + print(f"RESULTS:") + print(f" Lines: {len(result['lines'])} → {len(result['restored_lines'])}") + print(f"\n ★ HAIR COUNT: {result['num_hairs']} ★") + print(f"{'='*60}") + else: + results, failed = process_folder(args.folder, args.out, params) \ No newline at end of file diff --git a/pipeline.py b/pipeline.py new file mode 100644 index 0000000000000000000000000000000000000000..72ab89871c058cdbaaa10d96c4c5532c41b4b814 --- /dev/null +++ b/pipeline.py @@ -0,0 +1,784 @@ +import os +import csv +import sys +import glob +import json +import cv2 +import numpy as np +import torch +import warnings +from tqdm import tqdm +from PIL import Image +from skimage import io, transform +from torch.autograd import Variable +from torch.utils.data import DataLoader +from torchvision import transforms + +# Adjust paths to allow imports from subdirectories if necessary +# Assuming pipeline.py is in the root, we can import from segmentation and alopecia packages +# But since they don't have __init__.py, we might need to treat them as modules or just import carefully. +# Ideally, we should add __init__.py to them, but I will try to import assuming they are reachable. + +try: + from segmentation.data_loader import RescaleT, ToTensorLab, SalObjDataset + from segmentation.model import U2NET, U2NETP +except ImportError: + # Fallback if running from a different context, though we expect to run from root + sys.path.append(os.path.join(os.path.dirname(__file__), 'segmentation')) + from data_loader import RescaleT, ToTensorLab, SalObjDataset + from model import U2NET, U2NETP + +from segment_anything import sam_model_registry, SamPredictor + +# Import logic from alopecia scripts is harder because they are scripts, not modules with reusable functions easily exposed without refactoring. +# I will reimplement the logic here or import if possible. +# calculate_hair_thickness.py has functions: nms, find_pts_on_line, find_intersection_points2, get_direction2, main +# calculate_hair_count.py has functions: load_segment_mask, run_watershed_for_sep, apply_watershed_hierarchical, create_visualization, main + +# To avoid massive code duplication, I will try to import them. +# I might need to add __init__.py to make them importable or use sys.path. + +sys.path.append(os.path.join(os.getcwd(), 'alopecia')) +# Now we can try to import from them, but they are scripts. +# It's better to copy the helper functions to avoid running their main blocks if they are not guarded properly (they seem to be guarded). + +class ScalpPipeline: + def __init__(self, root_dir="."): + self.root_dir = os.path.abspath(root_dir) + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + # Default Paths + self.data_dir = os.path.join(self.root_dir, "datasets", "data") + self.seg_train_dir = os.path.join(self.root_dir, "datasets", "seg_train") + self.sam_val_dir = os.path.join(self.root_dir, "prediction", "sam_result", "sam_val") + self.ensemble_val_dir = os.path.join(self.root_dir, "prediction", "ensemble_result", "ensemble_val") + self.thickness_result_dir = os.path.join(self.root_dir, "alopecia", "thickness_result") + self.count_result_dir = os.path.join(self.root_dir, "alopecia", "count_result") + + # Model Paths + self.u2net_model_path = os.path.join(self.root_dir, "segmentation", "model", "U2NET.pth") + self.sam_checkpoint = os.path.join(self.root_dir, "sam_vit_h_4b8939.pth") + + # Ensure directories exist + for d in [self.seg_train_dir, self.sam_val_dir, self.ensemble_val_dir, self.thickness_result_dir, self.count_result_dir]: + os.makedirs(d, exist_ok=True) + + def normPRED(self, d): + ma = torch.max(d) + mi = torch.min(d) + dn = (d-mi)/(ma-mi) + return dn + + def save_output(self, image_name, pred, d_dir): + predict = pred + predict = predict.squeeze() + predict_np = predict.cpu().data.numpy() + + im = Image.fromarray(predict_np*255).convert('RGB') + img_name = image_name.split(os.sep)[-1] + image = io.imread(image_name) + imo = im.resize((image.shape[1],image.shape[0]),resample=Image.BILINEAR) + + pb_np = np.array(imo) + + aaa = img_name.split(".") + bbb = aaa[0:-1] + imidx = bbb[0] + for i in range(1,len(bbb)): + imidx = imidx + "." + bbb[i] + + imo.save(os.path.join(d_dir, imidx+'.jpg')) + + def run_u2net_segmentation(self): + print("\n🔹 Running U2NET Segmentation...") + model_name = 'u2net' + + img_name_list = glob.glob(os.path.join(self.data_dir, '*')) + if not img_name_list: + print(f"No images found in {self.data_dir}") + return + + test_salobj_dataset = SalObjDataset(img_name_list = img_name_list, + lbl_name_list = [], + transform=transforms.Compose([RescaleT(320), + ToTensorLab(flag=0)]) + ) + test_salobj_dataloader = DataLoader(test_salobj_dataset, + batch_size=1, + shuffle=False, + num_workers=1) + + if(model_name=='u2net'): + print("...load U2NET---173.6 MB") + net = U2NET(3,1) + + if torch.cuda.is_available(): + net.load_state_dict(torch.load(self.u2net_model_path)) + net.cuda() + else: + net.load_state_dict(torch.load(self.u2net_model_path, map_location='cpu')) + net.eval() + + for i_test, data_test in enumerate(test_salobj_dataloader): + print("inferencing:",img_name_list[i_test].split(os.sep)[-1]) + + inputs_test = data_test['image'] + inputs_test = inputs_test.type(torch.FloatTensor) + + if torch.cuda.is_available(): + inputs_test = Variable(inputs_test.cuda()) + else: + inputs_test = Variable(inputs_test) + + d1,d2,d3,d4,d5,d6,d7= net(inputs_test) + + # normalization + pred = d1[:,0,:,:] + pred = self.normPRED(pred) + + self.save_output(img_name_list[i_test], pred, self.seg_train_dir) + del d1,d2,d3,d4,d5,d6,d7 + + print("✅ U2NET Segmentation Complete.\n") + + # --- SAM Guide Helpers --- + def nms(self, boxes, thresh): + if len(boxes) == 0: + return [] + pick = [] + x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] + area = (x2 - x1 + 1) * (y2 - y1 + 1) + idxs = np.argsort(y2) + while len(idxs) > 0: + last = len(idxs) - 1 + i = idxs[last] + pick.append(i) + xx1 = np.maximum(x1[i], x1[idxs[:last]]) + yy1 = np.maximum(y1[i], y1[idxs[:last]]) + xx2 = np.minimum(x2[i], x2[idxs[:last]]) + yy2 = np.minimum(y2[i], y2[idxs[:last]]) + w = np.maximum(0, xx2 - xx1 + 1) + h = np.maximum(0, yy2 - yy1 + 1) + overlap = (w * h) / area[idxs[:last]] + idxs = np.delete(idxs, np.concatenate(([last], np.where(overlap > thresh)[0]))) + return boxes[pick] + + def cluster(self, img_path, im, save_dir): + img = cv2.imread(img_path) + imgray = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) + ret, binary_map = cv2.threshold(imgray, 127, 255, 0) + nlabels, labels, stats, centroids = cv2.connectedComponentsWithStats(binary_map, None, None, None, 8, cv2.CV_32S) + areas = stats[1:, cv2.CC_STAT_AREA] + result = np.zeros((labels.shape), np.uint8) + for i in range(0, nlabels - 1): + if areas[i] >= 250: + result[labels == i + 1] = 255 + re_copy = result.copy() + edgeimg = cv2.Canny(result, 10, 150) + skel = np.zeros(result.shape, np.uint8) + element = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3)) + while True: + open_ = cv2.morphologyEx(result, cv2.MORPH_OPEN, element) + temp = cv2.subtract(result, open_) + eroded = cv2.erode(result, element) + skel = cv2.bitwise_or(skel, temp) + result = eroded.copy() + if cv2.countNonZero(result) == 0: + break + nlabels, labels, stats, centroids = cv2.connectedComponentsWithStats(skel, None, None, None, 8, cv2.CV_32S) + areas = stats[1:, cv2.CC_STAT_AREA] + skel = np.zeros((labels.shape), np.uint8) + for i in range(0, nlabels - 1): + if areas[i] >= 2: + skel[labels == i + 1] = 255 + + # Save skeletons if needed, skipping for now or saving to temp + # base_name = os.path.splitext(im)[0] + # cv2.imwrite(os.path.join(save_dir, f"Skeleton_{base_name}.png"), skel) + + white_pixels = np.where(skel == 255) + x_coords, y_coords = white_pixels[1], white_pixels[0] + filter_size = (10, 10) + x1 = x_coords - filter_size[0] // 2 + y1 = y_coords - filter_size[1] // 2 + x2 = x_coords + filter_size[0] // 2 + y2 = y_coords + filter_size[1] // 2 + white_regions = np.column_stack((x1, y1, x2, y2)) + white_regions = self.nms(white_regions, thresh=0.1) + + center_points = [] + def get_direction2(bbox_pixels): + nonzero_indices = np.column_stack(np.nonzero(bbox_pixels)) + nonzero_indices = np.float32(nonzero_indices) + if len(nonzero_indices) >= 2: + mean, eigenvectors = cv2.PCACompute(nonzero_indices, mean=None) + cntr = ((mean[0, 1]), (mean[0, 0])) + return eigenvectors[0], cntr + else: + return (0, 0), (0, 0) + + for coor in white_regions: + x1, y1, x2, y2 = coor + bbox_pixels = skel[int(y1):int(y2), int(x1):int(x2)] + direction, mean = get_direction2(bbox_pixels) + center_points.append((mean[0] + x1, mean[1] + y1)) + + pts_group, bbox_group = [], [] + for idx, pts in enumerate(center_points): + if 640 > pts[0] > 0 and 480 > pts[1] > 0: + pts_group.append([int(pts[0]), int(pts[1])]) + x1, y1, x2, y2 = white_regions[idx] + bbox_group.append([int(x1), int(y1), int(x2), int(y2)]) + return pts_group, bbox_group + + def generate_sam_guides(self): + print("\n🔹 Generating SAM Guides (Points/BBox)...") + mask_dir = self.seg_train_dir + save_json_dir = os.path.join(self.root_dir, "datasets") + save_img_dir = os.path.join(save_json_dir, "output") + os.makedirs(save_img_dir, exist_ok=True) + + patterns = ['*.png', '*.jpg', '*.jpeg', '*.PNG', '*.JPG', '*.JPEG'] + files = [] + for p in patterns: + files.extend(glob.glob(os.path.join(mask_dir, p))) + files = sorted(set(files)) + print(f"Found {len(files)} files in {mask_dir}") + + file_dict = {} + bbox_dict = {} + + for filepath in tqdm(files): + filename = os.path.basename(filepath) + pts, bbox = self.cluster(filepath, filename, save_img_dir) + if len(pts) != 0: + file_dict[filename] = pts + bbox_dict[filename] = bbox + + with open(os.path.join(save_json_dir, 'train_seg_points.json'), 'w') as json_file: + json.dump(file_dict, json_file) + with open(os.path.join(save_json_dir, 'train_bbox_points.json'), 'w') as json_file: + json.dump(bbox_dict, json_file) + + print("✅ SAM Guides Generated.\n") + + def run_sam_prediction(self): + print("\n🔹 Running SAM Prediction...") + points_file = os.path.join(self.root_dir, 'datasets', 'train_seg_points.json') + if not os.path.exists(points_file): + print(f"Points file not found: {points_file}") + return + + with open(points_file, 'r') as f: + points = json.load(f) + + model_type = "vit_h" + sam = sam_model_registry[model_type](checkpoint=self.sam_checkpoint) + sam.to(device=self.device) + predictor = SamPredictor(sam) + + for full_name in tqdm(points.keys()): + name, ext = os.path.splitext(full_name) + sample_points = points.get(full_name) or points.get(f'{name}.png') or points.get(f'{name}.jpg') or points.get(f'{name}.jpeg') or [] + + possible_paths = [ + os.path.join(self.data_dir, f'{name}.jpeg'), + os.path.join(self.data_dir, f'{name}.jpg'), + os.path.join(self.data_dir, f'{name}.png'), + ] + image = None + for p in possible_paths: + if os.path.isfile(p): + image = cv2.imread(p) + break + if image is None or image.size == 0: + continue + + image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + predictor.set_image(np.ascontiguousarray(image)) + + if len(sample_points) == 0: + cv2.imwrite(os.path.join(self.sam_val_dir, f"{name}.jpg"), cv2.cvtColor(image, cv2.COLOR_RGB2BGR)) + continue + + tmp = np.array(sample_points) + tmp = tmp[tmp.min(axis=1) > 0] + + if len(tmp) == 0: + continue + + rand_idx = np.random.choice(len(tmp), max(1, len(tmp)//2), replace=False) + input_point = tmp[rand_idx] + + img_height, img_width = image.shape[:2] + neg_list = [] + border_width = 50 + + while len(neg_list) < 10: + side = np.random.choice(['top', 'bottom', 'left', 'right']) + if side == 'top': + xy = [np.random.randint(img_width), np.random.randint(0, border_width)] + elif side == 'bottom': + xy = [np.random.randint(img_width), np.random.randint(max(0, img_height-border_width), img_height)] + elif side == 'left': + xy = [np.random.randint(0, border_width), np.random.randint(img_height)] + else: + xy = [np.random.randint(max(0, img_width-border_width), img_width), np.random.randint(img_height)] + + if xy not in tmp.tolist(): + neg_list.append(xy) + + neg_arr = np.array(neg_list) + final_point = np.append(input_point, neg_arr).reshape(-1, 2) + input_label = np.array([0] * len(input_point) + [1] * len(neg_arr)) + + masks, scores, logits = predictor.predict( + point_coords=final_point, + point_labels=input_label, + multimask_output=True, + ) + + sam_mask = masks[np.argmax(scores)] + if sam_mask.ndim > 2: + sam_mask = sam_mask.squeeze() + + if sam_mask.shape != (img_height, img_width): + sam_mask = cv2.resize(sam_mask.astype(np.uint8), (img_width, img_height)) + + binary_map = np.where(sam_mask > 0, 0, 255).astype(np.uint8) + + nlabels, labels, stats, centroids = cv2.connectedComponentsWithStats( + binary_map, None, None, None, 8, cv2.CV_32S + ) + areas = stats[1:, cv2.CC_STAT_AREA] + result = np.zeros((labels.shape), np.uint8) + + for i in range(0, nlabels - 1): + if areas[i] >= 400: + result[labels == i + 1] = 255 + + save_path = os.path.join(self.sam_val_dir, f"{name}.jpg") + cv2.imwrite(save_path, result) + + print("✅ SAM Prediction Complete.\n") + + def create_ensemble_mask(self): + print("\n🔹 Creating Ensemble Masks...") + seg_path = self.seg_train_dir + sam_path = self.sam_val_dir + result_path = self.ensemble_val_dir + + seg_patterns = [os.path.join(seg_path, '*.png'), os.path.join(seg_path, '*.jpg'), os.path.join(seg_path, '*.jpeg')] + seg_full_path = [] + for pattern in seg_patterns: + seg_full_path.extend(sorted(glob.glob(pattern))) + seg_full_path = sorted(list(set(seg_full_path))) + + sam_patterns = [os.path.join(sam_path, '*.jpg'), os.path.join(sam_path, '*.png'), os.path.join(sam_path, '*.jpeg')] + sam_full_path = [] + for pattern in sam_patterns: + sam_full_path.extend(sorted(glob.glob(pattern))) + sam_full_path = sorted(list(set(sam_full_path))) + + seg_dict = {os.path.splitext(os.path.basename(p))[0]: p for p in seg_full_path} + sam_dict = {os.path.splitext(os.path.basename(p))[0]: p for p in sam_full_path} + + matched_pairs = [] + for name in seg_dict.keys(): + if name in sam_dict: + matched_pairs.append((seg_dict[name], sam_dict[name])) + + for seg, sam in tqdm(matched_pairs): + seg_img = cv2.imread(seg) + sam_img = cv2.imread(sam) + + if seg_img is None or sam_img is None: + continue + + if seg_img.shape != sam_img.shape: + sam_img = cv2.resize(sam_img, (seg_img.shape[1], seg_img.shape[0])) + + img_name = os.path.basename(sam) + added_img = cv2.bitwise_and(seg_img, sam_img) + binary_map = cv2.cvtColor(added_img, cv2.COLOR_BGR2GRAY) + + nlabels, labels, stats, centroids = cv2.connectedComponentsWithStats( + binary_map, None, None, None, 8, cv2.CV_32S + ) + areas = stats[1:, cv2.CC_STAT_AREA] + result = np.zeros((labels.shape), np.uint8) + for i in range(0, nlabels - 1): + if areas[i] >= 400: + result[labels == i + 1] = 255 + + cv2.imwrite(os.path.join(result_path, img_name), result) + + print("✅ Ensemble Masks Created.\n") + + # --- Metrics Calculation --- + def calculate_hair_thickness(self): + print("\n🔹 Calculating Hair Thickness...") + # Reimplementing logic from alopecia/calculate_hair_thickness.py + + def find_pts_on_line(og, slope, d): + cx, cy = og + x1 = cx - d / ((1 + slope ** 2) ** 0.5) + y1 = cy - slope * cx + x1 * slope + if np.isnan(x1) or np.isnan(y1): + x1 = y1 = -1 + return x1, y1 + + def find_intersection_points2(center, slope, img, threshold): + p2 = p1 = (-1, -1) + w, h = img.shape + step, searching_len = 100, 50 + for d in range(1, step * searching_len): + px, py = find_pts_on_line(center, slope, d / step) + if (0 < int(px) < h) and (0 < int(py) < w) and img[int(py)][int(px)] > threshold: + p1 = (px, py) + else: + break + for d in range(1, step * searching_len): + px, py = find_pts_on_line(center, slope, -d / step) + if (0 < int(px) < h) and (0 < int(py) < w) and img[int(py)][int(px)] > threshold: + p2 = (px, py) + else: + break + dst = 0 if p1 == (-1, -1) or p2 == (-1, -1) else np.linalg.norm(np.asarray(p1) - np.asarray(p2)) + return [p1, p2], dst + + def get_direction2(bbox_pixels): + nonzero_indices = np.column_stack(np.nonzero(bbox_pixels)) + nonzero_indices = np.float32(nonzero_indices) + if len(nonzero_indices) >= 2: + mean, eigenvectors = cv2.PCACompute(nonzero_indices, mean=None) + cntr = ((mean[0, 1]), (mean[0, 0])) + return eigenvectors[0], cntr + else: + return (0,0), (0,0) + + img_folder = self.ensemble_val_dir + save_path = self.thickness_result_dir + + for im_path in tqdm(sorted(glob.glob(os.path.join(img_folder, '*.jpg')))): + img = cv2.imread(im_path) + imgray = cv2.imread(im_path, cv2.IMREAD_GRAYSCALE) + img_name = os.path.splitext(os.path.basename(im_path))[0] + + if np.all(imgray == 255) or np.all(imgray == 0): + np.save(os.path.join(save_path, img_name), np.array([])) + continue + + ret, binary_map = cv2.threshold(imgray, 127, 255, 0) + nlabels, labels, stats, centroids = cv2.connectedComponentsWithStats(binary_map, None, None, None, 8, cv2.CV_32S) + areas = stats[1:, cv2.CC_STAT_AREA] + result = np.zeros((labels.shape), np.uint8) + for i in range(nlabels - 1): + if areas[i] >= 250: + result[labels == i + 1] = 255 + re_copy = result.copy() + + skel = np.zeros(result.shape, np.uint8) + element = cv2.getStructuringElement(cv2.MORPH_CROSS, (3,3)) + while True: + open_ = cv2.morphologyEx(result, cv2.MORPH_OPEN, element) + temp = cv2.subtract(result, open_) + eroded = cv2.erode(result, element) + skel = cv2.bitwise_or(skel, temp) + result = eroded.copy() + if cv2.countNonZero(result) == 0: + break + + nlabels, labels, stats, centroids = cv2.connectedComponentsWithStats(skel, None, None, None, 8, cv2.CV_32S) + areas = stats[1:, cv2.CC_STAT_AREA] + skel = np.zeros((labels.shape), np.uint8) + for i in range(nlabels - 1): + if areas[i] >= 5: + skel[labels == i + 1] = 255 + + filtered_image = cv2.cvtColor(re_copy, cv2.COLOR_GRAY2BGR) + filtered_image[skel == 255] = [0, 255, 0] + + white_pixels = np.where(skel == 255) + x_coords, y_coords = white_pixels[1], white_pixels[0] + filter_size = (20, 20) + x1, y1 = x_coords - filter_size[0]//2, y_coords - filter_size[1]//2 + x2, y2 = x_coords + filter_size[0]//2, y_coords + filter_size[1]//2 + white_regions = np.column_stack((x1, y1, x2, y2)) + white_regions = self.nms(white_regions, thresh=0.1) + + directions, center_points, thicknesses = [], [], [] + + for coor in white_regions: + x1, y1, x2, y2 = coor + bbox_pixels = skel[y1:y2, x1:x2] + direction, mean = get_direction2(bbox_pixels) + directions.append(direction) + center_points.append((mean[0] + x1, mean[1] + y1)) + + perpendicular_slope = [] + for direction in directions: + if direction[1] != 0: + perpendicular_slope.append(-1 / (direction[0] / direction[1])) + else: + perpendicular_slope.append(0) + + for center_point, perp_slope in zip(center_points, perpendicular_slope): + intersection, dst = find_intersection_points2(center_point, perp_slope, re_copy, 200) + if dst != 0: + thicknesses.append(dst) + if intersection[0] != (-1, -1) and intersection[1] != (-1, -1): + cv2.line(filtered_image, + (int(intersection[0][0]), int(intersection[0][1])), + (int(intersection[1][0]), int(intersection[1][1])), + (0, 255, 255), 1) + for pt in intersection: + cv2.circle(filtered_image, (int(pt[0]), int(pt[1])), 3, (0, 0, 255), -1) + + if len(thicknesses) > 0: + avg_thickness = np.mean(thicknesses) + cv2.putText(filtered_image, f"Avg thickness: {avg_thickness:.2f}px", + (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 0, 0), 2) + + save_img_path = os.path.join(save_path, f"{img_name}_vis.png") + cv2.imwrite(save_img_path, filtered_image) + np.save(os.path.join(save_path, img_name), np.sort(thicknesses)) + + print("✅ Hair Thickness Calculation Complete.\n") + + def calculate_hair_count(self): + print("\n🔹 Calculating Hair Count...") + # Reimplementing logic from alopecia/calculate_hair_count.py + + def load_segment_mask(img_path): + if not os.path.exists(img_path): return None + img_gray = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) + if img_gray is None: return None + kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) + binary_filtered = cv2.morphologyEx(img_gray, cv2.MORPH_OPEN, kernel) + _, binary_filtered = cv2.threshold(binary_filtered, 127, 255, cv2.THRESH_BINARY) + return binary_filtered + + def run_watershed_for_sep(binary_img, original_img, sep_factor): + dist_transform = cv2.distanceTransform(binary_img, cv2.DIST_L2, 5) + _, sure_fg = cv2.threshold(dist_transform, sep_factor * dist_transform.max(), 255, 0) + sure_fg = np.uint8(sure_fg) + kernel = np.ones((3,3), np.uint8) + sure_bg = cv2.dilate(binary_img, kernel, iterations=3) + unknown = cv2.subtract(sure_bg, sure_fg) + ret, markers = cv2.connectedComponents(sure_fg) + markers = markers + 1 + markers[unknown == 255] = 0 + if len(original_img.shape) == 2: + original_color = cv2.cvtColor(original_img, cv2.COLOR_GRAY2BGR) + else: + original_color = original_img.copy() + markers_w = markers.copy().astype(np.int32) + cv2.watershed(original_color, markers_w) + return markers_w + + def apply_watershed_hierarchical(binary_img, original_img, min_area, min_aspect_ratio, min_length, + separation_factor=0.2, hierarchy_levels=3): + low = max(0.01, separation_factor * 0.7) + high = separation_factor * 1.6 + if hierarchy_levels <= 1: + sep_levels = [separation_factor] + else: + sep_levels = list(np.linspace(low, high, hierarchy_levels)) + + markers_levels = [] + for s in sep_levels: + markers_levels.append(run_watershed_for_sep(binary_img, original_img, s)) + + current = markers_levels[0].copy().astype(np.int32) + next_label = int(current.max()) + 1 + + def region_props_from_mask(mask_uint8): + cnts, _ = cv2.findContours(mask_uint8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + props = [] + for cnt in cnts: + area = cv2.contourArea(cnt) + if area <= 0: continue + if len(cnt) >= 5: + try: + (x, y), (MA, ma), angle = cv2.fitEllipse(cnt) + except: + MA = ma = 0 + x = y = 0 + else: + x, y, w, h = cv2.boundingRect(cnt) + MA = max(w,h) + ma = min(w,h) + angle = 0 + minor = ma if ma > 0 else 1e-6 + aspect = float(max(MA, ma)) / (minor + 1e-6) + props.append({ + 'area': area, + 'major': max(MA, ma), + 'minor': minor, + 'aspect': aspect, + 'centroid': (float(x), float(y)) if 'x' in locals() else (0,0), + 'contour': cnt + }) + return props + + for lvl in range(1, len(markers_levels)): + finer = markers_levels[lvl] + new_current = current.copy() + unique_parents = np.unique(current) + for parent_label in unique_parents: + if parent_label <= 1: continue + parent_mask = (current == parent_label) + if parent_mask.sum() == 0: continue + overlapped = finer[parent_mask] + child_labels = np.unique(overlapped[(overlapped > 1)]) + if len(child_labels) <= 1: continue + + accepted_children = [] + for cl in child_labels: + child_mask = np.logical_and(finer == cl, parent_mask) + child_mask_uint8 = (child_mask.astype(np.uint8) * 255) + props = region_props_from_mask(child_mask_uint8) + if len(props) == 0: continue + p = max(props, key=lambda x: x['area']) + if p['area'] >= min_area and p['major'] >= min_length and p['aspect'] >= min_aspect_ratio: + accepted_children.append((child_mask_uint8, p)) + if len(accepted_children) >= 2: + new_current[parent_mask] = 0 + for (cmask_uint8, p) in accepted_children: + new_current[cmask_uint8 == 255] = next_label + next_label += 1 + current = new_current + + final_labels = current + valid_hairs = [] + unique_labels = np.unique(final_labels) + for label in unique_labels: + if label <= 1: continue + mask = (final_labels == label).astype(np.uint8) * 255 + cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + for cnt in cnts: + area = cv2.contourArea(cnt) + if area < min_area: continue + if len(cnt) < 5: continue + try: + (x, y), (MA, ma), angle = cv2.fitEllipse(cnt) + major_axis = max(MA, ma) + minor_axis = min(MA, ma) + aspect_ratio = major_axis / (minor_axis + 1e-6) + if major_axis >= min_length and aspect_ratio >= min_aspect_ratio: + valid_hairs.append({ + 'centroid': (x, y), + 'ellipse': ((x, y), (MA, ma), angle), + 'length': major_axis, + 'thickness': minor_axis, + 'area': area, + 'label': int(label) + }) + except Exception: + continue + return len(valid_hairs), valid_hairs + + def create_visualization(true_original, sam_background, hair_info, filename, save_dir): + h, w = true_original.shape[:2] + overlay = sam_background.copy() + if overlay.shape[:2] != (h, w): + overlay = cv2.resize(overlay, (w, h), interpolation=cv2.INTER_LINEAR) + for i, info in enumerate(hair_info): + cv2.ellipse(overlay, info['ellipse'], (0, 255, 0), 2) + cx, cy = map(int, info['centroid']) + if w > 300: + cv2.putText(overlay, str(i), (cx, cy), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255), 1) + border = np.zeros((h, 5, 3), dtype=np.uint8) + combined = np.hstack([true_original, border, overlay]) + header_height = 50 + header = np.zeros((header_height, combined.shape[1], 3), dtype=np.uint8) + info_text = f"{filename} | Count: {len(hair_info)}" + cv2.putText(header, info_text, (10, 35), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2) + final_vis = np.vstack([header, combined]) + cv2.imwrite(os.path.join(save_dir, f'vis_{filename}'), final_vis) + + img_folder = self.ensemble_val_dir + original_folder = self.data_dir + sam_folder = self.ensemble_val_dir # Using ensemble as SAM folder for visualization as per original script default + save_path = self.count_result_dir + + min_area = 1500 + min_length = 20 + min_ratio = 1.0 + separation_factor = 0.3 + hierarchy_levels = 2 + + img_names = [] + for ext in ['*.jpg', '*.png', '*.jpeg']: + full_paths = glob.glob(os.path.join(img_folder, ext)) + img_names.extend([os.path.basename(p) for p in full_paths]) + + results = {} + density_results = {} + + for im in tqdm(img_names, desc="Processing"): + segment_path = os.path.join(img_folder, im) + original_path = os.path.join(original_folder, im) + sam_path_file = os.path.join(sam_folder, im) + + if not os.path.exists(segment_path): continue + binary = load_segment_mask(segment_path) + if binary is None: continue + true_original = cv2.imread(original_path) + if true_original is None: + true_original = np.zeros((binary.shape[0], binary.shape[1], 3), dtype=np.uint8) + sam_background = cv2.imread(sam_path_file) + if sam_background is None: + sam_background = cv2.cvtColor(binary, cv2.COLOR_GRAY2BGR) + + hair_count, hair_info = apply_watershed_hierarchical( + binary, + true_original, + min_area=min_area, + min_aspect_ratio=min_ratio, + min_length=min_length, + separation_factor=separation_factor, + hierarchy_levels=hierarchy_levels + ) + + density_data = { + 'count': hair_count, + 'avg_thickness': float(np.mean([h['thickness'] for h in hair_info]) if hair_info else 0), + 'avg_length': float(np.mean([h['length'] for h in hair_info]) if hair_info else 0) + } + + if hair_count > 0 or density_data: + results[im] = hair_count + density_results[im] = density_data + + vis_dir = os.path.join(save_path, 'visualizations') + os.makedirs(vis_dir, exist_ok=True) + create_visualization(true_original, sam_background, hair_info, im, vis_dir) + + csv_path = os.path.join(save_path, 'hair_count.csv') + with open(csv_path, 'w', newline='') as f: + w = csv.writer(f) + w.writerow(['image_name', 'hair_count']) + for k, v in results.items(): + w.writerow([k, v]) + + json_path = os.path.join(save_path, 'density.json') + with open(json_path, 'w') as f: + json.dump(density_results, f, indent=2) + + print("✅ Hair Count Calculation Complete.\n") + + def run_pipeline(self): + print("🚀 Starting ScalpPipeline...") + self.run_u2net_segmentation() + self.generate_sam_guides() + self.run_sam_prediction() + self.create_ensemble_mask() + self.calculate_hair_thickness() + self.calculate_hair_count() + print("🎉 Pipeline Completed Successfully!") + +if __name__ == "__main__": + pipeline = ScalpPipeline() + pipeline.run_pipeline() diff --git a/prediction/.DS_Store b/prediction/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..a4c905e1402f876b2f17c1d62810e8a38e05b5f6 Binary files /dev/null and b/prediction/.DS_Store differ diff --git a/prediction/ensemble_result/.DS_Store b/prediction/ensemble_result/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..59281ad8165ac80acd52807d254d68a95a4ffa16 Binary files /dev/null and b/prediction/ensemble_result/.DS_Store differ diff --git a/prediction/ensemble_result/ensemble_val/230219_A191_1.jpg b/prediction/ensemble_result/ensemble_val/230219_A191_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..896896d125e501585b074f80416cb0a1b69f8ffa Binary files /dev/null and b/prediction/ensemble_result/ensemble_val/230219_A191_1.jpg differ diff --git a/prediction/ensemble_result/ensemble_val/230219_A191_1_test.jpg b/prediction/ensemble_result/ensemble_val/230219_A191_1_test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..89ee7b0f53679b7c9cbf113f39cbb1aa73019179 Binary files /dev/null and b/prediction/ensemble_result/ensemble_val/230219_A191_1_test.jpg differ diff --git a/prediction/ensemble_result/ensemble_val/230219_A200_4.jpg b/prediction/ensemble_result/ensemble_val/230219_A200_4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..841923cf19ca5ae35ba853d6afed4d867ad97d64 Binary files /dev/null and b/prediction/ensemble_result/ensemble_val/230219_A200_4.jpg differ diff --git a/prediction/ensemble_result/ensemble_val/IMG_02C458E4D7CE-3.jpg b/prediction/ensemble_result/ensemble_val/IMG_02C458E4D7CE-3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8db63825d0eec33cbc36732c93ddf7916edd0876 Binary files /dev/null and b/prediction/ensemble_result/ensemble_val/IMG_02C458E4D7CE-3.jpg differ diff --git a/prediction/ensemble_result/ensemble_val/IMG_02C458E4D7CE-4.jpg b/prediction/ensemble_result/ensemble_val/IMG_02C458E4D7CE-4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e903cfded051782fcb79cf5a071a8ca80f593c05 Binary files /dev/null and b/prediction/ensemble_result/ensemble_val/IMG_02C458E4D7CE-4.jpg differ diff --git a/prediction/ensemble_result/ensemble_val/IMG_02C458E4D7CE-5.jpg b/prediction/ensemble_result/ensemble_val/IMG_02C458E4D7CE-5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9ec83ccb0ab01d19b93573e1fe16bfa194d82921 --- /dev/null +++ b/prediction/ensemble_result/ensemble_val/IMG_02C458E4D7CE-5.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:080064ec0be9b48ebe8a6b4a2f6d7e7f801d2c80b5a3bdd85778561a805834f3 +size 108352 diff --git a/prediction/ensemble_result/ensemble_val/IMG_02C458E4D7CE-6.jpg b/prediction/ensemble_result/ensemble_val/IMG_02C458E4D7CE-6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ab31494aa8fe71c21f3cc25d79874cbed6aa3c12 Binary files /dev/null and b/prediction/ensemble_result/ensemble_val/IMG_02C458E4D7CE-6.jpg differ diff --git a/prediction/ensemble_result/ensemble_val/IMG_02C458E4D7CE-7.jpg b/prediction/ensemble_result/ensemble_val/IMG_02C458E4D7CE-7.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c305d3f8cc67e0c07297f3405e24128b563d0e92 --- /dev/null +++ b/prediction/ensemble_result/ensemble_val/IMG_02C458E4D7CE-7.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63a739759057589a20b51f0f467dd0ca3a1e6f8794caf7e2e042d16a79c54405 +size 135399 diff --git a/prediction/ensemble_result/ensemble_val/IMG_02C458E4D7CE-9.jpg b/prediction/ensemble_result/ensemble_val/IMG_02C458E4D7CE-9.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dc1bb22193dd3c96a86abbaa308aab6ec2d389c9 Binary files /dev/null and b/prediction/ensemble_result/ensemble_val/IMG_02C458E4D7CE-9.jpg differ diff --git a/prediction/ensemble_result/test_val/test_step7_merged_foreground_230219_A191_1.jpg b/prediction/ensemble_result/test_val/test_step7_merged_foreground_230219_A191_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..75b3c97754300b3767a2301abe6703124e2d9f24 Binary files /dev/null and b/prediction/ensemble_result/test_val/test_step7_merged_foreground_230219_A191_1.jpg differ diff --git a/prediction/ensemble_result/test_val/test_step7_merged_foreground_230219_A191_1_test.jpg b/prediction/ensemble_result/test_val/test_step7_merged_foreground_230219_A191_1_test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2e6f38645f1da6d53615b040bad24f96f22bfba3 Binary files /dev/null and b/prediction/ensemble_result/test_val/test_step7_merged_foreground_230219_A191_1_test.jpg differ diff --git a/prediction/ensemble_result/test_val/test_step7_merged_foreground_230219_A200_4.jpg b/prediction/ensemble_result/test_val/test_step7_merged_foreground_230219_A200_4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ce70926b9637b6311dd92de7763083b023436714 Binary files /dev/null and b/prediction/ensemble_result/test_val/test_step7_merged_foreground_230219_A200_4.jpg differ diff --git a/prediction/ensemble_result/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-3.jpeg b/prediction/ensemble_result/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-3.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..d5ca8d89e2af138ce6103a04a7fbc9f0bc47bac3 --- /dev/null +++ b/prediction/ensemble_result/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-3.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:895a6a54b3ed5f7fe8617ff57e8a6c9f5e765a204fed4faca4227bd962eaac4e +size 145301 diff --git a/prediction/ensemble_result/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-4.jpeg b/prediction/ensemble_result/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-4.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..2a111108644bd1ba1687faaae423ec1ea4abc69d Binary files /dev/null and b/prediction/ensemble_result/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-4.jpeg differ diff --git a/prediction/ensemble_result/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-5.jpeg b/prediction/ensemble_result/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-5.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..dc01095d5fa6501d52dc958ec365a1e565db0020 --- /dev/null +++ b/prediction/ensemble_result/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-5.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:471b11d2fcf9a54cb87b6e12a6a3e4b5be4fe0edad9445c46ca2872e31468134 +size 145189 diff --git a/prediction/ensemble_result/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-6.jpeg b/prediction/ensemble_result/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-6.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..15e0429db5f4e9a8a44994d5cd64ab45eb1adf1d --- /dev/null +++ b/prediction/ensemble_result/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-6.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a5e95a6ec26a27b00943d7fb0237df0e8dece5d8e7780d815ecd3293542c663 +size 144583 diff --git a/prediction/ensemble_result/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-7.jpeg b/prediction/ensemble_result/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-7.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..828b8d8d62087e1c2542df88e1afd29c2debe0b6 --- /dev/null +++ b/prediction/ensemble_result/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-7.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5dea4301f030fb9e45fb8f31a4277d94d37c7d7d87207584c641c3a97c5d8b73 +size 220850 diff --git a/prediction/ensemble_result/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-9.jpeg b/prediction/ensemble_result/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-9.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..0d3f30cee5172b94fd8068f57aa1c3abd4ac0453 --- /dev/null +++ b/prediction/ensemble_result/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-9.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7557db9c6168cc9be7fc061f9f2376a13c1d8c45c7c7128ce62c08a960868917 +size 101575 diff --git a/prediction/sam_result/sam_val/230219_A191_1.jpg b/prediction/sam_result/sam_val/230219_A191_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b72421044794dbd2aefe06b9f9b3b00085622443 --- /dev/null +++ b/prediction/sam_result/sam_val/230219_A191_1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:614ec8bb318db9e916072e9a30461d0b16badf2143a830e4e1978f527e8d0005 +size 108754 diff --git a/prediction/sam_result/sam_val/230219_A191_1_test.jpg b/prediction/sam_result/sam_val/230219_A191_1_test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e873da18cb3cacf1e12a0d95182c4effd3b584e3 --- /dev/null +++ b/prediction/sam_result/sam_val/230219_A191_1_test.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1e4aa3698617d9e56afca2a5212be5e5c71cdfb5d2ae63f47640896838ab257 +size 116305 diff --git a/prediction/sam_result/sam_val/230219_A200_4.jpg b/prediction/sam_result/sam_val/230219_A200_4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a58bd2c5a411968a25507c7cad36afcfcd3b3dbd --- /dev/null +++ b/prediction/sam_result/sam_val/230219_A200_4.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1469bad9426621df2c267f09a19fd10f95df2b1249658c1f2722d85a24c3f170 +size 112387 diff --git a/prediction/sam_result/sam_val/IMG_02C458E4D7CE-3.jpg b/prediction/sam_result/sam_val/IMG_02C458E4D7CE-3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8b2af9b09a63c61685d5652125804b4fab76d265 Binary files /dev/null and b/prediction/sam_result/sam_val/IMG_02C458E4D7CE-3.jpg differ diff --git a/prediction/sam_result/sam_val/IMG_02C458E4D7CE-4.jpg b/prediction/sam_result/sam_val/IMG_02C458E4D7CE-4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..91c39639b5f774593e12e0abfe5d43aa5072b049 Binary files /dev/null and b/prediction/sam_result/sam_val/IMG_02C458E4D7CE-4.jpg differ diff --git a/prediction/sam_result/sam_val/IMG_02C458E4D7CE-5.jpg b/prediction/sam_result/sam_val/IMG_02C458E4D7CE-5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c1d734723cf6e3a11c2ee3fa445eba969cfbb4f6 --- /dev/null +++ b/prediction/sam_result/sam_val/IMG_02C458E4D7CE-5.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:95ea00a6d0e5be14aa44d007ce1859bc9adbe9395e414a53d3af1c602a475a53 +size 111029 diff --git a/prediction/sam_result/sam_val/IMG_02C458E4D7CE-6.jpg b/prediction/sam_result/sam_val/IMG_02C458E4D7CE-6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2323f5bfe09a9b57ae96fc3c40fd72fa117bd74c Binary files /dev/null and b/prediction/sam_result/sam_val/IMG_02C458E4D7CE-6.jpg differ diff --git a/prediction/sam_result/sam_val/IMG_02C458E4D7CE-7.jpg b/prediction/sam_result/sam_val/IMG_02C458E4D7CE-7.jpg new file mode 100644 index 0000000000000000000000000000000000000000..368833505b9636dcddaf706300f2c58e5b4fd339 --- /dev/null +++ b/prediction/sam_result/sam_val/IMG_02C458E4D7CE-7.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9719240413c8b052ef4216f98a17a744450a936c6858abb9e34cdb003bd463f8 +size 149585 diff --git a/prediction/sam_result/sam_val/IMG_02C458E4D7CE-9.jpg b/prediction/sam_result/sam_val/IMG_02C458E4D7CE-9.jpg new file mode 100644 index 0000000000000000000000000000000000000000..66fff636204ddf2462ac3ad2ffe86962990992d5 Binary files /dev/null and b/prediction/sam_result/sam_val/IMG_02C458E4D7CE-9.jpg differ diff --git a/prediction/test_val/test_step7_merged_foreground_230219_A191_1.jpg b/prediction/test_val/test_step7_merged_foreground_230219_A191_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..53c1a998c2169f6469093fb04e34ef77a2b12497 Binary files /dev/null and b/prediction/test_val/test_step7_merged_foreground_230219_A191_1.jpg differ diff --git a/prediction/test_val/test_step7_merged_foreground_230219_A191_1_test.jpg b/prediction/test_val/test_step7_merged_foreground_230219_A191_1_test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..75cece220041e78350937ab9431691cfa92a72cd Binary files /dev/null and b/prediction/test_val/test_step7_merged_foreground_230219_A191_1_test.jpg differ diff --git a/prediction/test_val/test_step7_merged_foreground_230219_A200_4.jpg b/prediction/test_val/test_step7_merged_foreground_230219_A200_4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8393450fb38a98cb48d85df1a662289d186d7f11 Binary files /dev/null and b/prediction/test_val/test_step7_merged_foreground_230219_A200_4.jpg differ diff --git a/prediction/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-3.jpeg b/prediction/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-3.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..e315b8c3cfb4f207894b24f1c66d02f3c8d1a64a --- /dev/null +++ b/prediction/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-3.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3bc14886ffb4a4afd517cc7b39c69823db69c61f51248d40d8463a3dc8b5ce54 +size 145782 diff --git a/prediction/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-4.jpeg b/prediction/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-4.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..6892df8b6a2a9412bbbe85458b4bfe0b69584e73 Binary files /dev/null and b/prediction/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-4.jpeg differ diff --git a/prediction/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-5.jpeg b/prediction/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-5.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..40aab7b46b090c55772212c2e31aa75a724ed484 --- /dev/null +++ b/prediction/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-5.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b09f3715315895976b8f9ec5d5024a6b5a3a83e2d4675581df8d62471c9ae5dd +size 145766 diff --git a/prediction/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-6.jpeg b/prediction/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-6.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..687c95008375a3f03445363849da516d5e10c0ef --- /dev/null +++ b/prediction/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-6.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2392b0961ad289a2cc4c8a520fe489f705d417273544b85982fce437a4621ff4 +size 145255 diff --git a/prediction/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-7.jpeg b/prediction/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-7.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..20d25d744d6740ea552e3aa05b6ddf5ad357abb6 --- /dev/null +++ b/prediction/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-7.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb6dd574c41771acd6984e8afd440cc08634ea1d1f155688d7bc64fede4156a4 +size 221733 diff --git a/prediction/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-9.jpeg b/prediction/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-9.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..08b5b502d75659df41145a4e1c4b183c496e659d --- /dev/null +++ b/prediction/test_val/test_step7_merged_foreground_IMG_02C458E4D7CE-9.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a286b63fbb62ad96d1ce2060cae74e6ac73d7dbf7f045e3128fb6ba0c142d357 +size 101965 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..1f52385f9cb8e09bde6176ff5b87359169b498ad --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +torch == 2.0.1 +torchvision == 0.15.2 +timm == 0.9.2 +pandas +numpy diff --git a/sam_vit_h_4b8939.pth b/sam_vit_h_4b8939.pth new file mode 100644 index 0000000000000000000000000000000000000000..8523acce9ddab1cf7e355628a08b1aab8ce08a72 --- /dev/null +++ b/sam_vit_h_4b8939.pth @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7bf3b02f3ebf1267aba913ff637d9a2d5c33d3173bb679e46d9f338c26f262e +size 2564550879 diff --git a/segmentation/__init__.py b/segmentation/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/segmentation/__pycache__/__init__.cpython-39.pyc b/segmentation/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..22145620a08873ffed67de2497e284cad8682673 Binary files /dev/null and b/segmentation/__pycache__/__init__.cpython-39.pyc differ diff --git a/segmentation/__pycache__/data_loader.cpython-39.pyc b/segmentation/__pycache__/data_loader.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f1b4cc19727b8a9a00de2cf5c5de151ff0c841ba Binary files /dev/null and b/segmentation/__pycache__/data_loader.cpython-39.pyc differ diff --git a/segmentation/data_loader.py b/segmentation/data_loader.py new file mode 100644 index 0000000000000000000000000000000000000000..6f04d48e6ce124ad9f27bc2c9720cdb4d2241017 --- /dev/null +++ b/segmentation/data_loader.py @@ -0,0 +1,266 @@ +# data loader +from __future__ import print_function, division +import glob +import torch +from skimage import io, transform, color +import numpy as np +import random +import math +import matplotlib.pyplot as plt +from torch.utils.data import Dataset, DataLoader +from torchvision import transforms, utils +from PIL import Image + +#==========================dataset load========================== +class RescaleT(object): + + def __init__(self,output_size): + assert isinstance(output_size,(int,tuple)) + self.output_size = output_size + + def __call__(self,sample): + imidx, image, label = sample['imidx'], sample['image'],sample['label'] + + h, w = image.shape[:2] + + if isinstance(self.output_size,int): + if h > w: + new_h, new_w = self.output_size*h/w,self.output_size + else: + new_h, new_w = self.output_size,self.output_size*w/h + else: + new_h, new_w = self.output_size + + new_h, new_w = int(new_h), int(new_w) + + # #resize the image to new_h x new_w and convert image from range [0,255] to [0,1] + # img = transform.resize(image,(new_h,new_w),mode='constant') + # lbl = transform.resize(label,(new_h,new_w),mode='constant', order=0, preserve_range=True) + + img = transform.resize(image,(self.output_size,self.output_size),mode='constant') + lbl = transform.resize(label,(self.output_size,self.output_size),mode='constant', order=0, preserve_range=True) + + return {'imidx':imidx, 'image':img,'label':lbl} + +class Rescale(object): + + def __init__(self,output_size): + assert isinstance(output_size,(int,tuple)) + self.output_size = output_size + + def __call__(self,sample): + imidx, image, label = sample['imidx'], sample['image'],sample['label'] + + if random.random() >= 0.5: + image = image[::-1] + label = label[::-1] + + h, w = image.shape[:2] + + if isinstance(self.output_size,int): + if h > w: + new_h, new_w = self.output_size*h/w,self.output_size + else: + new_h, new_w = self.output_size,self.output_size*w/h + else: + new_h, new_w = self.output_size + + new_h, new_w = int(new_h), int(new_w) + + # #resize the image to new_h x new_w and convert image from range [0,255] to [0,1] + img = transform.resize(image,(new_h,new_w),mode='constant') + lbl = transform.resize(label,(new_h,new_w),mode='constant', order=0, preserve_range=True) + + return {'imidx':imidx, 'image':img,'label':lbl} + +class RandomCrop(object): + + def __init__(self,output_size): + assert isinstance(output_size, (int, tuple)) + if isinstance(output_size, int): + self.output_size = (output_size, output_size) + else: + assert len(output_size) == 2 + self.output_size = output_size + def __call__(self,sample): + imidx, image, label = sample['imidx'], sample['image'], sample['label'] + + if random.random() >= 0.5: + image = image[::-1] + label = label[::-1] + + h, w = image.shape[:2] + new_h, new_w = self.output_size + + top = np.random.randint(0, h - new_h) + left = np.random.randint(0, w - new_w) + + image = image[top: top + new_h, left: left + new_w] + label = label[top: top + new_h, left: left + new_w] + + return {'imidx':imidx,'image':image, 'label':label} + +class ToTensor(object): + """Convert ndarrays in sample to Tensors.""" + + def __call__(self, sample): + + imidx, image, label = sample['imidx'], sample['image'], sample['label'] + + tmpImg = np.zeros((image.shape[0],image.shape[1],3)) + tmpLbl = np.zeros(label.shape) + + image = image/np.max(image) + if(np.max(label)<1e-6): + label = label + else: + label = label/np.max(label) + + if image.shape[2]==1: + tmpImg[:,:,0] = (image[:,:,0]-0.485)/0.229 + tmpImg[:,:,1] = (image[:,:,0]-0.485)/0.229 + tmpImg[:,:,2] = (image[:,:,0]-0.485)/0.229 + else: + tmpImg[:,:,0] = (image[:,:,0]-0.485)/0.229 + tmpImg[:,:,1] = (image[:,:,1]-0.456)/0.224 + tmpImg[:,:,2] = (image[:,:,2]-0.406)/0.225 + + tmpLbl[:,:,0] = label[:,:,0] + + + tmpImg = tmpImg.transpose((2, 0, 1)) + tmpLbl = label.transpose((2, 0, 1)) + + return {'imidx':torch.from_numpy(imidx.copy()).float(), 'image': torch.from_numpy(tmpImg.copy()).float(), 'label': torch.from_numpy(tmpLbl.copy()).float()} + +class ToTensorLab(object): + """Convert ndarrays in sample to Tensors.""" + def __init__(self,flag=0): + self.flag = flag + + def __call__(self, sample): + + imidx, image, label =sample['imidx'], sample['image'], sample['label'] + + tmpLbl = np.zeros(label.shape) + + if(np.max(label)<1e-6): + label = label + else: + label = label/np.max(label) + + # change the color space + if self.flag == 2: # with rgb and Lab colors + tmpImg = np.zeros((image.shape[0],image.shape[1],6)) + tmpImgt = np.zeros((image.shape[0],image.shape[1],3)) + if image.shape[2]==1: + tmpImgt[:,:,0] = image[:,:,0] + tmpImgt[:,:,1] = image[:,:,0] + tmpImgt[:,:,2] = image[:,:,0] + else: + tmpImgt = image + tmpImgtl = color.rgb2lab(tmpImgt) + + # nomalize image to range [0,1] + tmpImg[:,:,0] = (tmpImgt[:,:,0]-np.min(tmpImgt[:,:,0]))/(np.max(tmpImgt[:,:,0])-np.min(tmpImgt[:,:,0])) + tmpImg[:,:,1] = (tmpImgt[:,:,1]-np.min(tmpImgt[:,:,1]))/(np.max(tmpImgt[:,:,1])-np.min(tmpImgt[:,:,1])) + tmpImg[:,:,2] = (tmpImgt[:,:,2]-np.min(tmpImgt[:,:,2]))/(np.max(tmpImgt[:,:,2])-np.min(tmpImgt[:,:,2])) + tmpImg[:,:,3] = (tmpImgtl[:,:,0]-np.min(tmpImgtl[:,:,0]))/(np.max(tmpImgtl[:,:,0])-np.min(tmpImgtl[:,:,0])) + tmpImg[:,:,4] = (tmpImgtl[:,:,1]-np.min(tmpImgtl[:,:,1]))/(np.max(tmpImgtl[:,:,1])-np.min(tmpImgtl[:,:,1])) + tmpImg[:,:,5] = (tmpImgtl[:,:,2]-np.min(tmpImgtl[:,:,2]))/(np.max(tmpImgtl[:,:,2])-np.min(tmpImgtl[:,:,2])) + + # tmpImg = tmpImg/(np.max(tmpImg)-np.min(tmpImg)) + + tmpImg[:,:,0] = (tmpImg[:,:,0]-np.mean(tmpImg[:,:,0]))/np.std(tmpImg[:,:,0]) + tmpImg[:,:,1] = (tmpImg[:,:,1]-np.mean(tmpImg[:,:,1]))/np.std(tmpImg[:,:,1]) + tmpImg[:,:,2] = (tmpImg[:,:,2]-np.mean(tmpImg[:,:,2]))/np.std(tmpImg[:,:,2]) + tmpImg[:,:,3] = (tmpImg[:,:,3]-np.mean(tmpImg[:,:,3]))/np.std(tmpImg[:,:,3]) + tmpImg[:,:,4] = (tmpImg[:,:,4]-np.mean(tmpImg[:,:,4]))/np.std(tmpImg[:,:,4]) + tmpImg[:,:,5] = (tmpImg[:,:,5]-np.mean(tmpImg[:,:,5]))/np.std(tmpImg[:,:,5]) + + elif self.flag == 1: #with Lab color + tmpImg = np.zeros((image.shape[0],image.shape[1],3)) + + if image.shape[2]==1: + tmpImg[:,:,0] = image[:,:,0] + tmpImg[:,:,1] = image[:,:,0] + tmpImg[:,:,2] = image[:,:,0] + else: + tmpImg = image + + tmpImg = color.rgb2lab(tmpImg) + + # tmpImg = tmpImg/(np.max(tmpImg)-np.min(tmpImg)) + + tmpImg[:,:,0] = (tmpImg[:,:,0]-np.min(tmpImg[:,:,0]))/(np.max(tmpImg[:,:,0])-np.min(tmpImg[:,:,0])) + tmpImg[:,:,1] = (tmpImg[:,:,1]-np.min(tmpImg[:,:,1]))/(np.max(tmpImg[:,:,1])-np.min(tmpImg[:,:,1])) + tmpImg[:,:,2] = (tmpImg[:,:,2]-np.min(tmpImg[:,:,2]))/(np.max(tmpImg[:,:,2])-np.min(tmpImg[:,:,2])) + + tmpImg[:,:,0] = (tmpImg[:,:,0]-np.mean(tmpImg[:,:,0]))/np.std(tmpImg[:,:,0]) + tmpImg[:,:,1] = (tmpImg[:,:,1]-np.mean(tmpImg[:,:,1]))/np.std(tmpImg[:,:,1]) + tmpImg[:,:,2] = (tmpImg[:,:,2]-np.mean(tmpImg[:,:,2]))/np.std(tmpImg[:,:,2]) + + else: # with rgb color + tmpImg = np.zeros((image.shape[0],image.shape[1],3)) + image = image/np.max(image) + if image.shape[2]==1: + tmpImg[:,:,0] = (image[:,:,0]-0.485)/0.229 + tmpImg[:,:,1] = (image[:,:,0]-0.485)/0.229 + tmpImg[:,:,2] = (image[:,:,0]-0.485)/0.229 + else: + tmpImg[:,:,0] = (image[:,:,0]-0.485)/0.229 + tmpImg[:,:,1] = (image[:,:,1]-0.456)/0.224 + tmpImg[:,:,2] = (image[:,:,2]-0.406)/0.225 + + tmpLbl[:,:,0] = label[:,:,0] + + + tmpImg = tmpImg.transpose((2, 0, 1)) + tmpLbl = label.transpose((2, 0, 1)) + + return {'imidx':torch.from_numpy(imidx.copy()).float(), 'image': torch.from_numpy(tmpImg.copy()).float(), 'label': torch.from_numpy(tmpLbl.copy()).float()} + +class SalObjDataset(Dataset): + def __init__(self,img_name_list,lbl_name_list,transform=None): + # self.root_dir = root_dir + # self.image_name_list = glob.glob(image_dir+'*.png') + # self.label_name_list = glob.glob(label_dir+'*.png') + self.image_name_list = img_name_list + self.label_name_list = lbl_name_list + self.transform = transform + + def __len__(self): + return len(self.image_name_list) + + def __getitem__(self,idx): + + # image = Image.open(self.image_name_list[idx])#io.imread(self.image_name_list[idx]) + # label = Image.open(self.label_name_list[idx])#io.imread(self.label_name_list[idx]) + + image = io.imread(self.image_name_list[idx]) + imname = self.image_name_list[idx] + imidx = np.array([idx]) + + if(0==len(self.label_name_list)): + label_3 = np.zeros(image.shape) + else: + label_3 = io.imread(self.label_name_list[idx]) + + label = np.zeros(label_3.shape[0:2]) + if(3==len(label_3.shape)): + label = label_3[:,:,0] + elif(2==len(label_3.shape)): + label = label_3 + + if(3==len(image.shape) and 2==len(label.shape)): + label = label[:,:,np.newaxis] + elif(2==len(image.shape) and 2==len(label.shape)): + image = image[:,:,np.newaxis] + label = label[:,:,np.newaxis] + + sample = {'imidx':imidx, 'image':image, 'label':label} + + if self.transform: + sample = self.transform(sample) + + return sample diff --git a/segmentation/make_final_mask.py b/segmentation/make_final_mask.py new file mode 100644 index 0000000000000000000000000000000000000000..65498a7402cdc3478c6d8c7ff86ee6aa873ebb92 --- /dev/null +++ b/segmentation/make_final_mask.py @@ -0,0 +1,118 @@ +import cv2 +import glob +from tqdm import tqdm +import numpy as np +import os + +def make_final_mask(seg_path, sam_path, result_path): + # Try multiple extensions for seg_path + seg_patterns = [ + os.path.join(seg_path, '*.png'), + os.path.join(seg_path, '*.jpg'), + os.path.join(seg_path, '*.jpeg'), + ] + + seg_full_path = [] + for pattern in seg_patterns: + files = sorted(glob.glob(pattern)) + seg_full_path.extend(files) + seg_full_path = sorted(list(set(seg_full_path))) # Remove duplicates + + # Try multiple extensions for sam_path + sam_patterns = [ + os.path.join(sam_path, '*.jpg'), + os.path.join(sam_path, '*.png'), + os.path.join(sam_path, '*.jpeg'), + ] + + sam_full_path = [] + for pattern in sam_patterns: + files = sorted(glob.glob(pattern)) + sam_full_path.extend(files) + sam_full_path = sorted(list(set(sam_full_path))) + + # DEBUG + print(f"[DEBUG] seg_path: {seg_path}") + print(f"[DEBUG] Found {len(seg_full_path)} seg images") + if len(seg_full_path) > 0: + print(f"[DEBUG] First 3 seg files: {seg_full_path[:3]}") + + print(f"[DEBUG] sam_path: {sam_path}") + print(f"[DEBUG] Found {len(sam_full_path)} sam images") + if len(sam_full_path) > 0: + print(f"[DEBUG] First 3 sam files: {sam_full_path[:3]}") + + if len(seg_full_path) == 0: + print(f"[ERROR] No seg images found in {seg_path}") + return + + if len(sam_full_path) == 0: + print(f"[ERROR] No sam images found in {sam_path}") + return + + # Match by filename (without extension) + seg_dict = {} + for path in seg_full_path: + basename = os.path.splitext(os.path.basename(path))[0] + seg_dict[basename] = path + + sam_dict = {} + for path in sam_full_path: + basename = os.path.splitext(os.path.basename(path))[0] + sam_dict[basename] = path + + # Find matching pairs + matched_pairs = [] + for name in seg_dict.keys(): + if name in sam_dict: + matched_pairs.append((seg_dict[name], sam_dict[name])) + + print(f"[INFO] Found {len(matched_pairs)} matching pairs") + + if len(matched_pairs) == 0: + print("[ERROR] No matching pairs found!") + print(f"[DEBUG] Seg basenames: {list(seg_dict.keys())[:5]}") + print(f"[DEBUG] Sam basenames: {list(sam_dict.keys())[:5]}") + return + + for seg, sam in tqdm(matched_pairs): + seg_img = cv2.imread(seg) + sam_img = cv2.imread(sam) + + if seg_img is None: + print(f"[WARN] Failed to read seg image: {seg}") + continue + if sam_img is None: + print(f"[WARN] Failed to read sam image: {sam}") + continue + + # Resize if shapes don't match + if seg_img.shape != sam_img.shape: + print(f"[INFO] Resizing sam {sam_img.shape} to match seg {seg_img.shape}") + sam_img = cv2.resize(sam_img, (seg_img.shape[1], seg_img.shape[0])) + + img_name = os.path.basename(sam) + added_img = cv2.bitwise_and(seg_img, sam_img) + binary_map = cv2.cvtColor(added_img, cv2.COLOR_BGR2GRAY) + + nlabels, labels, stats, centroids = cv2.connectedComponentsWithStats( + binary_map, None, None, None, 8, cv2.CV_32S + ) + + # Get CC_STAT_AREA component as stats[label, COLUMN] + areas = stats[1:, cv2.CC_STAT_AREA] + result = np.zeros((labels.shape), np.uint8) + for i in range(0, nlabels - 1): + if areas[i] >= 400: # Keep + result[labels == i + 1] = 255 + + output_path = os.path.join(result_path, img_name) + cv2.imwrite(output_path, result) + print(f"[INFO] Saved: {output_path}") + +if __name__ == '__main__': + seg_path = '/Users/Admin/ScalpVision/datasets/seg_train' # mask gốc + sam_path = '/Users/Admin/ScalpVision/prediction/sam_result/sam_val' # mask SAM + result_path = 'prediction/ensemble_result/ensemble_val' # output mask hợp nhất + os.makedirs(result_path, exist_ok=True) + make_final_mask(seg_path, sam_path, result_path) \ No newline at end of file diff --git a/segmentation/model/U2NET.pth b/segmentation/model/U2NET.pth new file mode 100644 index 0000000000000000000000000000000000000000..14dab850d2f6b71bc1acaf4ea392cececbb7f09c --- /dev/null +++ b/segmentation/model/U2NET.pth @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63b662d06bdc4e6b592a5d1928c790b0b1c5d8feaee2173e28a5e44ac96ca317 +size 176463525 diff --git a/segmentation/model/__init__.py b/segmentation/model/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5b758fba4351379b08c2db49c3a6c3a9f804ca34 --- /dev/null +++ b/segmentation/model/__init__.py @@ -0,0 +1,2 @@ +from .u2net import U2NET +from .u2net import U2NETP \ No newline at end of file diff --git a/segmentation/model/__pycache__/__init__.cpython-39.pyc b/segmentation/model/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e1e24608a9f60c619c82553c60cee8e1607822d5 Binary files /dev/null and b/segmentation/model/__pycache__/__init__.cpython-39.pyc differ diff --git a/segmentation/model/__pycache__/u2net.cpython-39.pyc b/segmentation/model/__pycache__/u2net.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7b45af0440b80096fbf1e928d3d396de181ebfcd Binary files /dev/null and b/segmentation/model/__pycache__/u2net.cpython-39.pyc differ diff --git a/segmentation/model/u2net.py b/segmentation/model/u2net.py new file mode 100644 index 0000000000000000000000000000000000000000..6a0650bcbeb7bad2e27e9f0c69b9b08f7dd39f92 --- /dev/null +++ b/segmentation/model/u2net.py @@ -0,0 +1,525 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + +class REBNCONV(nn.Module): + def __init__(self,in_ch=3,out_ch=3,dirate=1): + super(REBNCONV,self).__init__() + + self.conv_s1 = nn.Conv2d(in_ch,out_ch,3,padding=1*dirate,dilation=1*dirate) + self.bn_s1 = nn.BatchNorm2d(out_ch) + self.relu_s1 = nn.ReLU(inplace=True) + + def forward(self,x): + + hx = x + xout = self.relu_s1(self.bn_s1(self.conv_s1(hx))) + + return xout + +## upsample tensor 'src' to have the same spatial size with tensor 'tar' +def _upsample_like(src,tar): + + src = F.upsample(src,size=tar.shape[2:],mode='bilinear') + + return src + + +### RSU-7 ### +class RSU7(nn.Module):#UNet07DRES(nn.Module): + + def __init__(self, in_ch=3, mid_ch=12, out_ch=3): + super(RSU7,self).__init__() + + self.rebnconvin = REBNCONV(in_ch,out_ch,dirate=1) + + self.rebnconv1 = REBNCONV(out_ch,mid_ch,dirate=1) + self.pool1 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv2 = REBNCONV(mid_ch,mid_ch,dirate=1) + self.pool2 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv3 = REBNCONV(mid_ch,mid_ch,dirate=1) + self.pool3 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv4 = REBNCONV(mid_ch,mid_ch,dirate=1) + self.pool4 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv5 = REBNCONV(mid_ch,mid_ch,dirate=1) + self.pool5 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv6 = REBNCONV(mid_ch,mid_ch,dirate=1) + + self.rebnconv7 = REBNCONV(mid_ch,mid_ch,dirate=2) + + self.rebnconv6d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv5d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv4d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv3d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv2d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv1d = REBNCONV(mid_ch*2,out_ch,dirate=1) + + def forward(self,x): + + hx = x + hxin = self.rebnconvin(hx) + + hx1 = self.rebnconv1(hxin) + hx = self.pool1(hx1) + + hx2 = self.rebnconv2(hx) + hx = self.pool2(hx2) + + hx3 = self.rebnconv3(hx) + hx = self.pool3(hx3) + + hx4 = self.rebnconv4(hx) + hx = self.pool4(hx4) + + hx5 = self.rebnconv5(hx) + hx = self.pool5(hx5) + + hx6 = self.rebnconv6(hx) + + hx7 = self.rebnconv7(hx6) + + hx6d = self.rebnconv6d(torch.cat((hx7,hx6),1)) + hx6dup = _upsample_like(hx6d,hx5) + + hx5d = self.rebnconv5d(torch.cat((hx6dup,hx5),1)) + hx5dup = _upsample_like(hx5d,hx4) + + hx4d = self.rebnconv4d(torch.cat((hx5dup,hx4),1)) + hx4dup = _upsample_like(hx4d,hx3) + + hx3d = self.rebnconv3d(torch.cat((hx4dup,hx3),1)) + hx3dup = _upsample_like(hx3d,hx2) + + hx2d = self.rebnconv2d(torch.cat((hx3dup,hx2),1)) + hx2dup = _upsample_like(hx2d,hx1) + + hx1d = self.rebnconv1d(torch.cat((hx2dup,hx1),1)) + + return hx1d + hxin + +### RSU-6 ### +class RSU6(nn.Module):#UNet06DRES(nn.Module): + + def __init__(self, in_ch=3, mid_ch=12, out_ch=3): + super(RSU6,self).__init__() + + self.rebnconvin = REBNCONV(in_ch,out_ch,dirate=1) + + self.rebnconv1 = REBNCONV(out_ch,mid_ch,dirate=1) + self.pool1 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv2 = REBNCONV(mid_ch,mid_ch,dirate=1) + self.pool2 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv3 = REBNCONV(mid_ch,mid_ch,dirate=1) + self.pool3 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv4 = REBNCONV(mid_ch,mid_ch,dirate=1) + self.pool4 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv5 = REBNCONV(mid_ch,mid_ch,dirate=1) + + self.rebnconv6 = REBNCONV(mid_ch,mid_ch,dirate=2) + + self.rebnconv5d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv4d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv3d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv2d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv1d = REBNCONV(mid_ch*2,out_ch,dirate=1) + + def forward(self,x): + + hx = x + + hxin = self.rebnconvin(hx) + + hx1 = self.rebnconv1(hxin) + hx = self.pool1(hx1) + + hx2 = self.rebnconv2(hx) + hx = self.pool2(hx2) + + hx3 = self.rebnconv3(hx) + hx = self.pool3(hx3) + + hx4 = self.rebnconv4(hx) + hx = self.pool4(hx4) + + hx5 = self.rebnconv5(hx) + + hx6 = self.rebnconv6(hx5) + + + hx5d = self.rebnconv5d(torch.cat((hx6,hx5),1)) + hx5dup = _upsample_like(hx5d,hx4) + + hx4d = self.rebnconv4d(torch.cat((hx5dup,hx4),1)) + hx4dup = _upsample_like(hx4d,hx3) + + hx3d = self.rebnconv3d(torch.cat((hx4dup,hx3),1)) + hx3dup = _upsample_like(hx3d,hx2) + + hx2d = self.rebnconv2d(torch.cat((hx3dup,hx2),1)) + hx2dup = _upsample_like(hx2d,hx1) + + hx1d = self.rebnconv1d(torch.cat((hx2dup,hx1),1)) + + return hx1d + hxin + +### RSU-5 ### +class RSU5(nn.Module):#UNet05DRES(nn.Module): + + def __init__(self, in_ch=3, mid_ch=12, out_ch=3): + super(RSU5,self).__init__() + + self.rebnconvin = REBNCONV(in_ch,out_ch,dirate=1) + + self.rebnconv1 = REBNCONV(out_ch,mid_ch,dirate=1) + self.pool1 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv2 = REBNCONV(mid_ch,mid_ch,dirate=1) + self.pool2 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv3 = REBNCONV(mid_ch,mid_ch,dirate=1) + self.pool3 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv4 = REBNCONV(mid_ch,mid_ch,dirate=1) + + self.rebnconv5 = REBNCONV(mid_ch,mid_ch,dirate=2) + + self.rebnconv4d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv3d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv2d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv1d = REBNCONV(mid_ch*2,out_ch,dirate=1) + + def forward(self,x): + + hx = x + + hxin = self.rebnconvin(hx) + + hx1 = self.rebnconv1(hxin) + hx = self.pool1(hx1) + + hx2 = self.rebnconv2(hx) + hx = self.pool2(hx2) + + hx3 = self.rebnconv3(hx) + hx = self.pool3(hx3) + + hx4 = self.rebnconv4(hx) + + hx5 = self.rebnconv5(hx4) + + hx4d = self.rebnconv4d(torch.cat((hx5,hx4),1)) + hx4dup = _upsample_like(hx4d,hx3) + + hx3d = self.rebnconv3d(torch.cat((hx4dup,hx3),1)) + hx3dup = _upsample_like(hx3d,hx2) + + hx2d = self.rebnconv2d(torch.cat((hx3dup,hx2),1)) + hx2dup = _upsample_like(hx2d,hx1) + + hx1d = self.rebnconv1d(torch.cat((hx2dup,hx1),1)) + + return hx1d + hxin + +### RSU-4 ### +class RSU4(nn.Module):#UNet04DRES(nn.Module): + + def __init__(self, in_ch=3, mid_ch=12, out_ch=3): + super(RSU4,self).__init__() + + self.rebnconvin = REBNCONV(in_ch,out_ch,dirate=1) + + self.rebnconv1 = REBNCONV(out_ch,mid_ch,dirate=1) + self.pool1 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv2 = REBNCONV(mid_ch,mid_ch,dirate=1) + self.pool2 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv3 = REBNCONV(mid_ch,mid_ch,dirate=1) + + self.rebnconv4 = REBNCONV(mid_ch,mid_ch,dirate=2) + + self.rebnconv3d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv2d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv1d = REBNCONV(mid_ch*2,out_ch,dirate=1) + + def forward(self,x): + + hx = x + + hxin = self.rebnconvin(hx) + + hx1 = self.rebnconv1(hxin) + hx = self.pool1(hx1) + + hx2 = self.rebnconv2(hx) + hx = self.pool2(hx2) + + hx3 = self.rebnconv3(hx) + + hx4 = self.rebnconv4(hx3) + + hx3d = self.rebnconv3d(torch.cat((hx4,hx3),1)) + hx3dup = _upsample_like(hx3d,hx2) + + hx2d = self.rebnconv2d(torch.cat((hx3dup,hx2),1)) + hx2dup = _upsample_like(hx2d,hx1) + + hx1d = self.rebnconv1d(torch.cat((hx2dup,hx1),1)) + + return hx1d + hxin + +### RSU-4F ### +class RSU4F(nn.Module):#UNet04FRES(nn.Module): + + def __init__(self, in_ch=3, mid_ch=12, out_ch=3): + super(RSU4F,self).__init__() + + self.rebnconvin = REBNCONV(in_ch,out_ch,dirate=1) + + self.rebnconv1 = REBNCONV(out_ch,mid_ch,dirate=1) + self.rebnconv2 = REBNCONV(mid_ch,mid_ch,dirate=2) + self.rebnconv3 = REBNCONV(mid_ch,mid_ch,dirate=4) + + self.rebnconv4 = REBNCONV(mid_ch,mid_ch,dirate=8) + + self.rebnconv3d = REBNCONV(mid_ch*2,mid_ch,dirate=4) + self.rebnconv2d = REBNCONV(mid_ch*2,mid_ch,dirate=2) + self.rebnconv1d = REBNCONV(mid_ch*2,out_ch,dirate=1) + + def forward(self,x): + + hx = x + + hxin = self.rebnconvin(hx) + + hx1 = self.rebnconv1(hxin) + hx2 = self.rebnconv2(hx1) + hx3 = self.rebnconv3(hx2) + + hx4 = self.rebnconv4(hx3) + + hx3d = self.rebnconv3d(torch.cat((hx4,hx3),1)) + hx2d = self.rebnconv2d(torch.cat((hx3d,hx2),1)) + hx1d = self.rebnconv1d(torch.cat((hx2d,hx1),1)) + + return hx1d + hxin + + +##### U^2-Net #### +class U2NET(nn.Module): + + def __init__(self,in_ch=3,out_ch=1): + super(U2NET,self).__init__() + + self.stage1 = RSU7(in_ch,32,64) + self.pool12 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.stage2 = RSU6(64,32,128) + self.pool23 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.stage3 = RSU5(128,64,256) + self.pool34 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.stage4 = RSU4(256,128,512) + self.pool45 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.stage5 = RSU4F(512,256,512) + self.pool56 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.stage6 = RSU4F(512,256,512) + + # decoder + self.stage5d = RSU4F(1024,256,512) + self.stage4d = RSU4(1024,128,256) + self.stage3d = RSU5(512,64,128) + self.stage2d = RSU6(256,32,64) + self.stage1d = RSU7(128,16,64) + + self.side1 = nn.Conv2d(64,out_ch,3,padding=1) + self.side2 = nn.Conv2d(64,out_ch,3,padding=1) + self.side3 = nn.Conv2d(128,out_ch,3,padding=1) + self.side4 = nn.Conv2d(256,out_ch,3,padding=1) + self.side5 = nn.Conv2d(512,out_ch,3,padding=1) + self.side6 = nn.Conv2d(512,out_ch,3,padding=1) + + self.outconv = nn.Conv2d(6*out_ch,out_ch,1) + + def forward(self,x): + + hx = x + + #stage 1 + hx1 = self.stage1(hx) + hx = self.pool12(hx1) + + #stage 2 + hx2 = self.stage2(hx) + hx = self.pool23(hx2) + + #stage 3 + hx3 = self.stage3(hx) + hx = self.pool34(hx3) + + #stage 4 + hx4 = self.stage4(hx) + hx = self.pool45(hx4) + + #stage 5 + hx5 = self.stage5(hx) + hx = self.pool56(hx5) + + #stage 6 + hx6 = self.stage6(hx) + hx6up = _upsample_like(hx6,hx5) + + #-------------------- decoder -------------------- + hx5d = self.stage5d(torch.cat((hx6up,hx5),1)) + hx5dup = _upsample_like(hx5d,hx4) + + hx4d = self.stage4d(torch.cat((hx5dup,hx4),1)) + hx4dup = _upsample_like(hx4d,hx3) + + hx3d = self.stage3d(torch.cat((hx4dup,hx3),1)) + hx3dup = _upsample_like(hx3d,hx2) + + hx2d = self.stage2d(torch.cat((hx3dup,hx2),1)) + hx2dup = _upsample_like(hx2d,hx1) + + hx1d = self.stage1d(torch.cat((hx2dup,hx1),1)) + + + #side output + d1 = self.side1(hx1d) + + d2 = self.side2(hx2d) + d2 = _upsample_like(d2,d1) + + d3 = self.side3(hx3d) + d3 = _upsample_like(d3,d1) + + d4 = self.side4(hx4d) + d4 = _upsample_like(d4,d1) + + d5 = self.side5(hx5d) + d5 = _upsample_like(d5,d1) + + d6 = self.side6(hx6) + d6 = _upsample_like(d6,d1) + + d0 = self.outconv(torch.cat((d1,d2,d3,d4,d5,d6),1)) + + return F.sigmoid(d0), F.sigmoid(d1), F.sigmoid(d2), F.sigmoid(d3), F.sigmoid(d4), F.sigmoid(d5), F.sigmoid(d6) + +### U^2-Net small ### +class U2NETP(nn.Module): + + def __init__(self,in_ch=3,out_ch=1): + super(U2NETP,self).__init__() + + self.stage1 = RSU7(in_ch,16,64) + self.pool12 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.stage2 = RSU6(64,16,64) + self.pool23 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.stage3 = RSU5(64,16,64) + self.pool34 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.stage4 = RSU4(64,16,64) + self.pool45 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.stage5 = RSU4F(64,16,64) + self.pool56 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.stage6 = RSU4F(64,16,64) + + # decoder + self.stage5d = RSU4F(128,16,64) + self.stage4d = RSU4(128,16,64) + self.stage3d = RSU5(128,16,64) + self.stage2d = RSU6(128,16,64) + self.stage1d = RSU7(128,16,64) + + self.side1 = nn.Conv2d(64,out_ch,3,padding=1) + self.side2 = nn.Conv2d(64,out_ch,3,padding=1) + self.side3 = nn.Conv2d(64,out_ch,3,padding=1) + self.side4 = nn.Conv2d(64,out_ch,3,padding=1) + self.side5 = nn.Conv2d(64,out_ch,3,padding=1) + self.side6 = nn.Conv2d(64,out_ch,3,padding=1) + + self.outconv = nn.Conv2d(6*out_ch,out_ch,1) + + def forward(self,x): + + hx = x + + #stage 1 + hx1 = self.stage1(hx) + hx = self.pool12(hx1) + + #stage 2 + hx2 = self.stage2(hx) + hx = self.pool23(hx2) + + #stage 3 + hx3 = self.stage3(hx) + hx = self.pool34(hx3) + + #stage 4 + hx4 = self.stage4(hx) + hx = self.pool45(hx4) + + #stage 5 + hx5 = self.stage5(hx) + hx = self.pool56(hx5) + + #stage 6 + hx6 = self.stage6(hx) + hx6up = _upsample_like(hx6,hx5) + + #decoder + hx5d = self.stage5d(torch.cat((hx6up,hx5),1)) + hx5dup = _upsample_like(hx5d,hx4) + + hx4d = self.stage4d(torch.cat((hx5dup,hx4),1)) + hx4dup = _upsample_like(hx4d,hx3) + + hx3d = self.stage3d(torch.cat((hx4dup,hx3),1)) + hx3dup = _upsample_like(hx3d,hx2) + + hx2d = self.stage2d(torch.cat((hx3dup,hx2),1)) + hx2dup = _upsample_like(hx2d,hx1) + + hx1d = self.stage1d(torch.cat((hx2dup,hx1),1)) + + + #side output + d1 = self.side1(hx1d) + + d2 = self.side2(hx2d) + d2 = _upsample_like(d2,d1) + + d3 = self.side3(hx3d) + d3 = _upsample_like(d3,d1) + + d4 = self.side4(hx4d) + d4 = _upsample_like(d4,d1) + + d5 = self.side5(hx5d) + d5 = _upsample_like(d5,d1) + + d6 = self.side6(hx6) + d6 = _upsample_like(d6,d1) + + d0 = self.outconv(torch.cat((d1,d2,d3,d4,d5,d6),1)) + + return F.sigmoid(d0), F.sigmoid(d1), F.sigmoid(d2), F.sigmoid(d3), F.sigmoid(d4), F.sigmoid(d5), F.sigmoid(d6) \ No newline at end of file diff --git a/segmentation/sam_guide.py b/segmentation/sam_guide.py new file mode 100644 index 0000000000000000000000000000000000000000..05f9fff970853cafd5690c6029f3bb618d258e34 --- /dev/null +++ b/segmentation/sam_guide.py @@ -0,0 +1,159 @@ +import cv2, os, json +import numpy as np +from math import atan2, cos, sin, sqrt, pi +from sklearn.cluster import DBSCAN +from sklearn.preprocessing import StandardScaler +from sklearn.cluster import OPTICS, cluster_optics_dbscan +from tqdm import tqdm +from glob import glob + +def nms(boxes, thresh): + if len(boxes) == 0: + return [] + + pick = [] + x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] + area = (x2 - x1 + 1) * (y2 - y1 + 1) + idxs = np.argsort(y2) + + while len(idxs) > 0: + last = len(idxs) - 1 + i = idxs[last] + pick.append(i) + + xx1 = np.maximum(x1[i], x1[idxs[:last]]) + yy1 = np.maximum(y1[i], y1[idxs[:last]]) + xx2 = np.minimum(x2[i], x2[idxs[:last]]) + yy2 = np.minimum(y2[i], y2[idxs[:last]]) + + w = np.maximum(0, xx2 - xx1 + 1) + h = np.maximum(0, yy2 - yy1 + 1) + + overlap = (w * h) / area[idxs[:last]] + idxs = np.delete(idxs, np.concatenate(([last], np.where(overlap > thresh)[0]))) + + return boxes[pick] + +def find_pts_on_line(og, slope, d): + cx, cy = og + x1 = float(cx - d / ((1 + slope**2)**0.5)) + y1 = float(cy - slope * cx + x1 * slope) + return (x1, y1) + +def cluster(img_path, im, save_dir): + img = cv2.imread(img_path) + imgray = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) + + ret, binary_map = cv2.threshold(imgray, 127, 255, 0) + + nlabels, labels, stats, centroids = cv2.connectedComponentsWithStats(binary_map, None, None, None, 8, cv2.CV_32S) + areas = stats[1:, cv2.CC_STAT_AREA] + result = np.zeros((labels.shape), np.uint8) + for i in range(0, nlabels - 1): + if areas[i] >= 250: + result[labels == i + 1] = 255 + re_copy = result.copy() + + edgeimg = cv2.Canny(result, 10, 150) + + size = np.size(result) + skel = np.zeros(result.shape, np.uint8) + element = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3)) + + while True: + open_ = cv2.morphologyEx(result, cv2.MORPH_OPEN, element) + temp = cv2.subtract(result, open_) + eroded = cv2.erode(result, element) + skel = cv2.bitwise_or(skel, temp) + result = eroded.copy() + if cv2.countNonZero(result) == 0: + break + + nlabels, labels, stats, centroids = cv2.connectedComponentsWithStats(skel, None, None, None, 8, cv2.CV_32S) + areas = stats[1:, cv2.CC_STAT_AREA] + skel = np.zeros((labels.shape), np.uint8) + for i in range(0, nlabels - 1): + if areas[i] >= 2: + skel[labels == i + 1] = 255 + + # Lưu riêng cho mỗi ảnh + base_name = os.path.splitext(im)[0] + os.makedirs(save_dir, exist_ok=True) + cv2.imwrite(os.path.join(save_dir, f"Skeleton_{base_name}.png"), skel) + + combined_img = cv2.addWeighted(skel, 1, edgeimg, 1, 0) + cv2.imwrite(os.path.join(save_dir, f"SkeletonContour_{base_name}.png"), combined_img) + + filter_size = (10, 10) + white_pixels = np.where(skel == 255) + x_coords, y_coords = white_pixels[1], white_pixels[0] + + x1 = x_coords - filter_size[0] // 2 + y1 = y_coords - filter_size[1] // 2 + x2 = x_coords + filter_size[0] // 2 + y2 = y_coords + filter_size[1] // 2 + + white_regions = np.column_stack((x1, y1, x2, y2)) + white_regions = nms(white_regions, thresh=0.1) + + skeleton_image = cv2.cvtColor(skel.copy(), cv2.COLOR_GRAY2BGR) + filtered_image = cv2.cvtColor(re_copy.copy(), cv2.COLOR_GRAY2BGR) + center_points, directions = [], [] + + def get_direction2(bbox_pixels): + nonzero_indices = np.column_stack(np.nonzero(bbox_pixels)) + nonzero_indices = np.float32(nonzero_indices) + if len(nonzero_indices) >= 2: + mean, eigenvectors = cv2.PCACompute(nonzero_indices, mean=None) + cntr = ((mean[0, 1]), (mean[0, 0])) + return eigenvectors[0], cntr + else: + return (0, 0), (0, 0) + + for coor in white_regions: + x1, y1, x2, y2 = coor + bbox_pixels = skel[int(y1):int(y2), int(x1):int(x2)] + direction, mean = get_direction2(bbox_pixels) + directions.append(direction) + center_points.append((mean[0] + x1, mean[1] + y1)) + cv2.rectangle(skeleton_image, (int(x1), int(y1)), (int(x2), int(y2)), (255, 255, 255), 1) + cv2.circle(skeleton_image, (int(mean[0] + x1), int(mean[1] + y1)), 1, (255, 0, 0), -1) + + cv2.imwrite(os.path.join(save_dir, f"Result_{base_name}.png"), skeleton_image) + + pts_group, bbox_group = [], [] + for idx, pts in enumerate(center_points): + if 640 > pts[0] > 0 and 480 > pts[1] > 0: + pts_group.append([int(pts[0]), int(pts[1])]) + x1, y1, x2, y2 = white_regions[idx] + bbox_group.append([int(x1), int(y1), int(x2), int(y2)]) + + return pts_group, bbox_group + + +mask_dir = "datasets/seg_train" +save_json_dir = "datasets" +save_img_dir = os.path.join(save_json_dir, "output") + +patterns = ['*.png', '*.jpg', '*.jpeg', '*.PNG', '*.JPG', '*.JPEG'] +files = [] +for p in patterns: + files.extend(glob(os.path.join(mask_dir, p))) +files = sorted(set(files)) +print(f"Found {len(files)} files in {mask_dir}") + +file_dict = {} +bbox_dict = {} + +for filepath in tqdm(files): + filename = os.path.basename(filepath) + pts, bbox = cluster(filepath, filename, save_img_dir) + if len(pts) != 0: + file_dict[filename] = pts + bbox_dict[filename] = bbox + +with open(os.path.join(save_json_dir, 'train_seg_points.json'), 'w') as json_file: + json.dump(file_dict, json_file) + +with open(os.path.join(save_json_dir, 'train_bbox_points.json'), 'w') as json_file: + json.dump(bbox_dict, json_file) diff --git a/segmentation/sam_predict.py b/segmentation/sam_predict.py new file mode 100644 index 0000000000000000000000000000000000000000..81e7b5b724e76bfcee44fe34eeb83d5067e56e8b --- /dev/null +++ b/segmentation/sam_predict.py @@ -0,0 +1,148 @@ +from segment_anything import sam_model_registry, SamAutomaticMaskGenerator, SamPredictor +import matplotlib.pyplot as plt +import numpy as np +import cv2 +from tqdm import tqdm +import json +import os +import torch + +# Load sample points +with open('datasets/train_seg_points.json', 'r') as f: + points = json.load(f) + +sam_checkpoint = "sam_vit_h_4b8939.pth" +model_type = "vit_h" + +device = torch.device("cpu") +sam = sam_model_registry[model_type](checkpoint=sam_checkpoint) +sam.to(device=device) +predictor = SamPredictor(sam) + +output_dir = 'prediction/sam_result/sam_val' +os.makedirs(output_dir, exist_ok=True) + +# Verify output directory +abs_output_dir = os.path.abspath(output_dir) +print(f"[INFO] Output directory: {abs_output_dir}") +print(f"[INFO] Directory exists: {os.path.exists(abs_output_dir)}") +print(f"[INFO] Directory writable: {os.access(abs_output_dir, os.W_OK)}") + +for full_name in tqdm(points.keys()): + name, ext = os.path.splitext(full_name) + sample_points = points.get(full_name) or points.get(f'{name}.png') or points.get(f'{name}.jpg') or points.get(f'{name}.jpeg') or [] + + # Find input image + possible_paths = [ + os.path.join('datasets', 'data', f'{name}.jpeg'), + os.path.join('datasets', 'data', f'{name}.jpg'), + os.path.join('datasets', 'data', f'{name}.png'), + ] + image = None + for p in possible_paths: + if os.path.isfile(p): + image = cv2.imread(p) + print(f"Using image: {p}") + break + if image is None or image.size == 0: + print(f"[WARN] No valid image found for {name}, skipping.") + continue + + # Prepare image for SAM + image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + predictor.set_image(np.ascontiguousarray(image)) + + # If no points, save original image + if len(sample_points) == 0: + print(f"[INFO] No points for {name}, saving original image.") + cv2.imwrite(os.path.join(output_dir, f"{name}.jpg"), cv2.cvtColor(image, cv2.COLOR_RGB2BGR)) + continue + + # Filter valid points + tmp = np.array(sample_points) + tmp = tmp[tmp.min(axis=1) > 0] + + # Use 50% of hair points randomly + rand_idx = np.random.choice(len(tmp), len(tmp)//2, replace=False) + input_point = tmp[rand_idx] # 50% hair points + + # ✅ UPDATED: Smart negative points from border regions + img_height, img_width = image.shape[:2] + neg_list = [] + border_width = 50 + + while len(neg_list) < 10: + side = np.random.choice(['top', 'bottom', 'left', 'right']) + if side == 'top': + xy = [np.random.randint(img_width), np.random.randint(0, border_width)] + elif side == 'bottom': + xy = [np.random.randint(img_width), np.random.randint(img_height-border_width, img_height)] + elif side == 'left': + xy = [np.random.randint(0, border_width), np.random.randint(img_height)] + else: # right + xy = [np.random.randint(img_width-border_width, img_width), np.random.randint(img_height)] + + if xy not in tmp.tolist(): + neg_list.append(xy) + + neg_arr = np.array(neg_list) # scalp points (from borders) + + # Combine points + final_point = np.append(input_point, neg_arr).reshape(-1, 2) + + # LOGIC CODE 2: Label assignment (0=hair, 1=scalp) - KEPT AS ORIGINAL + input_label = np.array([0] * len(input_point) + [1] * len(neg_arr)) + + print(f"[INFO] Using {len(input_point)} hair points (label=0) and {len(neg_arr)} scalp points (label=1)") + print(f"[INFO] Using ORIGINAL label logic (0=hair, 1=scalp)") + print(f"[INFO] Negative points: smart border sampling") + + # Predict mask + masks, scores, logits = predictor.predict( + point_coords=final_point, + point_labels=input_label, + multimask_output=True, + ) + + # Get best mask + sam_mask = masks[np.argmax(scores)] + + # Ensure 2D + if sam_mask.ndim > 2: + sam_mask = sam_mask.squeeze() + + # Resize if needed + if sam_mask.shape != (img_height, img_width): + sam_mask = cv2.resize(sam_mask.astype(np.uint8), (img_width, img_height)) + + # LOGIC CODE 2: Binary map INVERTED (hair=0/black, bg=255/white) - KEPT AS ORIGINAL + binary_map = np.where(sam_mask > 0, 0, 255).astype(np.uint8) + print(f"[INFO] Using ORIGINAL binary map logic (hair=0/black, bg=255/white)") + + # Get rid of noises (small white spots that are not hair) + nlabels, labels, stats, centroids = cv2.connectedComponentsWithStats( + binary_map, None, None, None, 8, cv2.CV_32S + ) + + # Get CC_STAT_AREA component + areas = stats[1:, cv2.CC_STAT_AREA] + result = np.zeros((labels.shape), np.uint8) + kept_components = 0 + + for i in range(0, nlabels - 1): + if areas[i] >= 400: # Keep components >= 400 pixels + result[labels == i + 1] = 255 + kept_components += 1 + + print(f"[INFO] Found {nlabels-1} components, kept {kept_components} (>= 400 pixels)") + + # Save result + save_path = os.path.join(output_dir, f"{name}.jpg") + success = cv2.imwrite(save_path, result) + + if success: + file_size = os.path.getsize(save_path) + print(f"[INFO] Saved mask for {name} at {save_path} ({file_size} bytes)") + else: + print(f"[ERROR] cv2.imwrite failed for {name}") + print("="*80) \ No newline at end of file diff --git a/segmentation/u2net_test.py b/segmentation/u2net_test.py new file mode 100644 index 0000000000000000000000000000000000000000..fa8999e9d5d3b828912571cb1241b1af8bea1141 --- /dev/null +++ b/segmentation/u2net_test.py @@ -0,0 +1,122 @@ +import os +from skimage import io, transform +import torch +import torchvision +from torch.autograd import Variable +import torch.nn as nn +import torch.nn.functional as F +from torch.utils.data import Dataset, DataLoader +from torchvision import transforms#, utils +# import torch.optim as optim + +import numpy as np +from PIL import Image +import glob + +from data_loader import RescaleT +from data_loader import ToTensor +from data_loader import ToTensorLab +from data_loader import SalObjDataset + +from model import U2NET # full size version 173.6 MB +from model import U2NETP # small version u2net 4.7 MB + +# normalize the predicted SOD probability map +def normPRED(d): + ma = torch.max(d) + mi = torch.min(d) + + dn = (d-mi)/(ma-mi) + + return dn + +def save_output(image_name,pred,d_dir): + + predict = pred + predict = predict.squeeze() + predict_np = predict.cpu().data.numpy() + + im = Image.fromarray(predict_np*255).convert('RGB') + img_name = image_name.split(os.sep)[-1] + image = io.imread(image_name) + imo = im.resize((image.shape[1],image.shape[0]),resample=Image.BILINEAR) + + pb_np = np.array(imo) + + aaa = img_name.split(".") + bbb = aaa[0:-1] + imidx = bbb[0] + for i in range(1,len(bbb)): + imidx = imidx + "." + bbb[i] + + imo.save(d_dir+imidx+'.jpg') + +def main(): + + # --------- 1. get image path and name --------- + model_name='u2net'#u2netp + + + + # image_dir = '../../../scalp_aihub/sample/' + image_dir = 'datasets/data' + prediction_dir = 'datasets/seg_train/' + model_dir = 'segmentation/model/U2NET.pth' + + img_name_list = glob.glob(os.path.join(image_dir, '*')) + + # --------- 2. dataloader --------- + #1. dataloader + test_salobj_dataset = SalObjDataset(img_name_list = img_name_list, + lbl_name_list = [], + transform=transforms.Compose([RescaleT(320), + ToTensorLab(flag=0)]) + ) + test_salobj_dataloader = DataLoader(test_salobj_dataset, + batch_size=1, + shuffle=False, + num_workers=1) + + # --------- 3. model define --------- + if(model_name=='u2net'): + print("...load U2NET---173.6 MB") + net = U2NET(3,1) + elif(model_name=='u2netp'): + print("...load U2NEP---4.7 MB") + net = U2NETP(3,1) + + if torch.cuda.is_available(): + net.load_state_dict(torch.load(model_dir)) + net.cuda() + else: + net.load_state_dict(torch.load(model_dir, map_location='cpu')) + net.eval() + + # --------- 4. inference for each image --------- + for i_test, data_test in enumerate(test_salobj_dataloader): + + print("inferencing:",img_name_list[i_test].split(os.sep)[-1]) + + inputs_test = data_test['image'] + inputs_test = inputs_test.type(torch.FloatTensor) + + if torch.cuda.is_available(): + inputs_test = Variable(inputs_test.cuda()) + else: + inputs_test = Variable(inputs_test) + + d1,d2,d3,d4,d5,d6,d7= net(inputs_test) + + # normalization + pred = d1[:,0,:,:] + pred = normPRED(pred) + + # save results to test_results folder + if not os.path.exists(prediction_dir): + os.makedirs(prediction_dir, exist_ok=True) + save_output(img_name_list[i_test],pred,prediction_dir) + + del d1,d2,d3,d4,d5,d6,d7 + +if __name__ == "__main__": + main()