diff --git a/.gitignore b/.gitignore index 25e6771..d334382 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ orisys.egg-info/ /dist_single_pyd/ /old_orisys/ /dist_nuitka/ -/build_nuitka/ \ No newline at end of file +/build_nuitka +/data/ \ No newline at end of file diff --git a/config/cal.npy b/config/cal.npy index cf2d210..36f9a7b 100644 Binary files a/config/cal.npy and b/config/cal.npy differ diff --git a/config/compound_eye.png b/config/compound_eye.png index ef3b17b..5bf2d0a 100644 Binary files a/config/compound_eye.png and b/config/compound_eye.png differ diff --git a/example1_basic_pipeline.py b/example1_basic_pipeline.py index b6aeb68..182aaf0 100644 --- a/example1_basic_pipeline.py +++ b/example1_basic_pipeline.py @@ -19,7 +19,8 @@ def main(): # 步骤1:创建传感器对象 (必要步骤),config_name为配置文件名(内置配置文件名)或路径(自定义配置文件路径) # 方式1: 从视频文件读取 - sensor = orisys.Sensor("./s1.mp4", isstitch=True, cuda=True, verbose=True,config_name="prototype") + sensor = orisys.Sensor("./ov2710_uv_2.mp4", isstitch=True, cuda=True, verbose=True,config_name="./config/lens_test.json") + # 方式2: 从摄像头读取(取消注释使用) # sensor = orisys.Sensor(0, cuda=True, verbose=True, config_name="prototype") @@ -31,7 +32,7 @@ def main(): # 步骤2:信息提取 # 步骤 2.1: 获取并拼接图像 (必要步骤) sensor.get_img() - + print("img_size_raw:", sensor.img_size_raw) # 步骤 2.2: 计算形变场 (必要步骤) sensor.compute_deformation() # 步骤 2.3: 计算触碰点 diff --git a/patch_generation/patch_generation.py b/patch_generation/patch_generation.py new file mode 100644 index 0000000..931db44 --- /dev/null +++ b/patch_generation/patch_generation.py @@ -0,0 +1,53 @@ +#%% +import numpy as np +import cv2 + +def generate_color_pattern(h, w, d, r): + # Initialize the pattern with zeros + pattern = np.zeros((h, w, 3)) + + # Calculate the number of patches in each dimension + num_patches_x = h // d + num_patches_y = w // d + + # Maximum number of attempts to find a suitable color + max_attempts = 1000 + + # Iterate over the patches + for i in range(num_patches_x): + for j in range(num_patches_y): + # Get the neighbors + neighbors = [] + if i > 0: + neighbors.append(pattern[(i-1)*d:i*d, j*d:(j+1)*d]) + if j > 0: + neighbors.append(pattern[i*d:(i+1)*d, (j-1)*d:j*d]) + + # Generate a new color + for attempt in range(max_attempts): + new_color = np.random.rand(3) + if all(np.min((new_color - neighbor.mean(axis=(0, 1)))**2) >= r for neighbor in neighbors): + break + if attempt == max_attempts - 1: + print(f"Warning: Could not find a suitable color for patch ({i}, {j}) after {max_attempts} attempts") + + # Apply the new color to the patch + pattern[i*d:(i+1)*d, j*d:(j+1)*d] = new_color + + print('Patch ({}, {})'.format(i, j)) + + return pattern + +# Test the function +pattern = generate_color_pattern(162, 162, 3, 0.75) + +# Convert the pattern to a format suitable for display with OpenCV +pattern_display = (pattern * 255).astype(np.uint8) + +# # Display the pattern +# cv2.imshow('Pattern', pattern_display) +# cv2.waitKey(0) +# cv2.destroyAllWindows() + +# #%% +cv2.imwrite('color_pattern.png', pattern_display) diff --git a/patch_generation/patch_generation_3.py b/patch_generation/patch_generation_3.py new file mode 100644 index 0000000..5c4a07c --- /dev/null +++ b/patch_generation/patch_generation_3.py @@ -0,0 +1,139 @@ +#%% +import numpy as np +import cv2 + +def merge_intervals(intervals): + # Merge overlapping intervals. + if not intervals: + return [] + intervals.sort(key=lambda x: x[0]) + merged = [intervals[0]] + for current in intervals[1:]: + last = merged[-1] + if current[0] <= last[1]: + merged[-1] = (last[0], max(last[1], current[1])) + else: + merged.append(current) + return merged + +def get_allowed_intervals(neighbor_values, delta): + # Compute forbidden intervals for this channel from neighbor values. + forbidden = [] + for c in neighbor_values: + low = max(0, c - delta) + high = min(1, c + delta) + forbidden.append((low, high)) + # Merge overlapping forbidden intervals. + merged = merge_intervals(forbidden) + allowed = [] + # Allowed region before the first forbidden interval. + if merged: + if merged[0][0] > 0: + allowed.append((0, merged[0][0])) + # Allowed regions between forbidden intervals. + for i in range(1, len(merged)): + allowed.append((merged[i-1][1], merged[i][0])) + # Allowed region after the last forbidden interval. + if merged[-1][1] < 1: + allowed.append((merged[-1][1], 1)) + else: + allowed.append((0, 1)) + return allowed + +def sample_from_allowed(allowed): + # Choose a random value from the allowed intervals with probability proportional to interval length. + lengths = [end - start for start, end in allowed] + total_length = sum(lengths) + if total_length <= 0: + return None + point = np.random.rand() * total_length + cumulative = 0 + for (start, end), length in zip(allowed, lengths): + cumulative += length + if point <= cumulative: + return np.random.uniform(start, end) + return allowed[-1][0] + +def sample_channel(neighbor_values, delta): + # Compute allowed intervals and sample a value. + allowed = get_allowed_intervals(neighbor_values, delta) + if not allowed: + return None + return sample_from_allowed(allowed) + +def generate_valid_color(neighbors, r): + """ + Generate a random color that satisfies, for each neighbor, + |R - R_neighbor| >= sqrt(r), and similarly for G and B. + neighbors: list of neighbor colors (each is an array of shape (3,)) + r: threshold for squared difference. + """ + delta = np.sqrt(r) + color = np.zeros(3) + for channel in range(3): + neighbor_channel = [n[channel] for n in neighbors] + sampled = sample_channel(neighbor_channel, delta) + # If the allowed intervals are empty, use a fallback. + if sampled is None: + sampled = np.random.rand() + color[channel] = sampled + return color + +def generate_color_pattern(h, w, d, r): + """ + Generate a dense color pattern. + h, w: height and width of the output image in pixels. + d: patch size in pixels (each patch is d x d). + r: randomness regulation factor. For each neighbor the condition is: + (|channel_new - channel_neighbor|)^2 >= r. + """ + pattern = np.zeros((h, w, 3), dtype=np.float32) + num_patches_x = h // d + num_patches_y = w // d + + for i in range(num_patches_x): + for j in range(num_patches_y): + # Collect neighbor patch colors (using mean because patches have uniform color). + neighbors = [] + if i > 0: + patch_above = pattern[(i-1)*d:i*d, j*d:(j+1)*d] + neighbors.append(np.mean(patch_above, axis=(0, 1))) + if j > 0: + patch_left = pattern[i*d:(i+1)*d, (j-1)*d:j*d] + neighbors.append(np.mean(patch_left, axis=(0, 1))) + + if len(neighbors) == 0: + # For the first patch use a random color. + new_color = np.random.rand(3) + else: + new_color = generate_valid_color(neighbors, r) + + pattern[i*d:(i+1)*d, j*d:(j+1)*d] = new_color + print('Patch ({}, {})'.format(i, j)) + return pattern + +def main(): + # Parameters for the pattern. + # For example, using 100x100 pixels image with patches of 3x3 pixels. + height = 200 + width = 200 + patch_size = 3 + # Set r to a value that gives sufficient difference. + # Note: r is the threshold for squared difference per channel, and the condition is + # |c - c_neighbor| >= sqrt(r). Very high r may make it impossible to satisfy. + r = 0.5 + + pattern = generate_color_pattern(height, width, patch_size, r) + # Scale to 0-255 for display and saving. + pattern_display = (pattern * 255).astype(np.uint8) + + # Display the pattern with OpenCV. + cv2.imshow('Pattern', pattern_display) + cv2.waitKey(0) + cv2.destroyAllWindows() + + # Save the pattern as an image file. + cv2.imwrite('color_pattern.png', pattern_display) + +if __name__ == '__main__': + main() diff --git a/patch_generation/test_hue_gray.py b/patch_generation/test_hue_gray.py new file mode 100644 index 0000000..6ecedc3 --- /dev/null +++ b/patch_generation/test_hue_gray.py @@ -0,0 +1,224 @@ +import cv2 +import numpy as np +import matplotlib.pyplot as plt +def calculate_local_contrast(image_input, window_size=5): + """ + Calculate Global Average Local Contrast (for fine texture detail evaluation in optical flow). + Args: + image_input: str (image file path) or numpy array (grayscale/RGB image). + window_size: int (odd number, default=5) — sliding window size for fine texture analysis. + Returns: + float: Global average local contrast (higher = better fine texture detail; target ≥0.2 for optical flow). + Raises: + ValueError: If image is invalid, window size is even/too small, or processing fails. + """ + # ---------------------- Step 1: Input Validation & Preprocessing ---------------------- + # Validate window size (must be odd, ≥3 to capture meaningful texture) + if window_size % 2 == 0 or window_size < 3: + raise ValueError(f"Window size must be an odd integer ≥3 (got {window_size}). Use 3/5/7.") + + # Load/convert image to grayscale (consistent with optical flow workflow) + if isinstance(image_input, str): + img = cv2.imread(image_input) + if img is None: + raise ValueError(f"Failed to load image from path: {image_input}") + elif isinstance(image_input, np.ndarray): + img = image_input.copy() + else: + raise ValueError("Input must be an image file path (str) or numpy array (image).") + + # Convert to grayscale (handle RGB/BGR) + if len(img.shape) == 3: + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + else: + gray = img # Already grayscale + + # Normalize to 0-255 (avoid non-standard pixel ranges, e.g., float32) + gray = cv2.normalize(gray, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) + + # ---------------------- Step 2: Sliding Window Local Contrast Calculation ---------------------- + # Define padding (replicate edge pixels to avoid boundary distortion) + pad_size = window_size // 2 + gray_padded = cv2.copyMakeBorder( + gray, + top=pad_size, bottom=pad_size, left=pad_size, right=pad_size, + borderType=cv2.BORDER_REPLICATE + ) + + # Initialize array to store local contrast for each window + local_contrast_vals = [] + + # Iterate over every pixel (center of the sliding window) + for y in range(pad_size, gray_padded.shape[0] - pad_size): + for x in range(pad_size, gray_padded.shape[1] - pad_size): + # Extract 5x5 (or custom) window around (x,y) + window = gray_padded[y-pad_size : y+pad_size+1, x-pad_size : x+pad_size+1] + + # Compute Michelson contrast for the window (avoid division by zero) + window_max = np.max(window) + window_min = np.min(window) + if window_max + window_min == 0: # All pixels are 0 (black window) + lc = 0.0 + else: + lc = (window_max - window_min) / (window_max + window_min) + + local_contrast_vals.append(lc) + + # ---------------------- Step 3: Global Average Local Contrast ---------------------- + global_local_contrast = np.mean(local_contrast_vals) if local_contrast_vals else 0.0 + + # Round for readability (4 decimal places) + return global_local_contrast + +def calculate_edge_strength_density(image_input): + """ + Calculate Edge Strength and Edge Density for optical flow evaluation. + Args: + image_input: Can be (1) str (image file path) or (2) numpy array (grayscale/RGB image). + Returns: + dict: Contains two metrics: + - edge_strength: Average gradient magnitude of valid edges (higher = sharper edges). + - edge_density: Percentage of valid edge pixels (higher = more texture/edges). + Raises: + ValueError: If image cannot be loaded/processed. + """ + # ---------------------- Step 1: Load/Preprocess Image ---------------------- + + img = image_input + # Convert to grayscale (handle RGB/BGR) + if len(img.shape) == 3: + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + else: + gray = img # Already grayscale + + # Normalize to 0-255 (avoid float/non-standard pixel ranges) + gray = cv2.normalize(gray, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) + + # ---------------------- Step 2: Compute Gradient Magnitude (Sobel) ---------------------- + # Sobel kernel size = 3 (balance between noise reduction and edge sharpness for optical flow) + sobel_x = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3) # Horizontal gradient + sobel_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3) # Vertical gradient + + # Compute gradient magnitude (edge strength raw values) + gradient_magnitude = np.sqrt(sobel_x**2 + sobel_y**2) + + # ---------------------- Step 3: Adaptive Threshold for Valid Edges ---------------------- + # Use median gradient to filter noise (robust to outliers) + median_gradient = np.median(gradient_magnitude) + # Threshold = 1.5× median (filters weak noise, keeps meaningful edges for optical flow) + edge_threshold = 1.5 * median_gradient + # Create binary mask for valid edges (1 = valid edge, 0 = non-edge) + valid_edge_mask = (gradient_magnitude > edge_threshold).astype(np.uint8) + + # ---------------------- Step 4: Calculate Metrics ---------------------- + # Edge Strength: Average gradient magnitude of valid edges + valid_gradient_values = gradient_magnitude[valid_edge_mask == 1] + edge_strength = np.mean(valid_gradient_values) if len(valid_gradient_values) > 0 else 0.0 + + # Edge Density: Percentage of valid edge pixels (total pixels = height × width) + total_pixels = gray.shape[0] * gray.shape[1] + valid_edge_pixels = np.sum(valid_edge_mask) + edge_density = (valid_edge_pixels / total_pixels) * 100 if total_pixels > 0 else 0.0 + # print(f"edge_strength: {edge_strength}, edge_density: {edge_density}") + + # Round for readability (3 decimal places) + return edge_strength, edge_density + +def custom_gray_conversion(img, r_weight=0.299, g_weight=0.587, b_weight=0.114, + red_boost=1.2, green_reduce=0.8): + """ + Custom grayscale calculation with tunable RGB weights + red/green contrast enhancement + Args: + image_path: Path to input image + r_weight: Weight for Red channel (default: 0.299) + g_weight: Weight for Green channel (default: 0.587) + b_weight: Weight for Blue channel (default: 0.114) + red_boost: Multiplier to boost red region grayscale (>1 = brighter) + green_reduce: Multiplier to reduce green region grayscale (<1 = darker) + Returns: + default_gray: Grayscale with standard weights + custom_gray: Grayscale with tunable weights + red/green enhancement + """ + # 1. Read image and validate + # img_bgr = cv2.imread(image_path) + img_bgr = img + + if img_bgr is None: + raise ValueError("Failed to read image - check path!") + + # 2. Convert BGR (OpenCV default) to RGB and split channels + img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) + # Convert to float32 to avoid integer overflow/precision loss + r = img_rgb[:, :, 0].astype(np.float32) + g = img_rgb[:, :, 1].astype(np.float32) + b = img_rgb[:, :, 2].astype(np.float32) + + # 3. Calculate DEFAULT grayscale (standard weights) + default_gray = 0.299 * r + 0.587 * g + 0.114 * b + + # 4. Calculate CUSTOM grayscale (tunable weights) + custom_gray_base = r_weight * r + g_weight * g + b_weight * b + + # 5. Enhance red/green contrast (optional but effective) + # Define red/green regions using RGB value ratios (no HSV needed) + # red_mask = (r > g * 1.2) & (r > b * 1.2) & (r >= 40) # Red-dominant pixels + # green_mask = (g > r * 1.2) & (g > b * 1.2) & (g >= 40) # Green-dominant pixels + + # Apply targeted adjustments to red/green regions + custom_gray = custom_gray_base.copy() + # custom_gray[red_mask] *= red_boost # Boost red region grayscale + # custom_gray[green_mask] *= green_reduce # Reduce green region grayscale + + # 6. Normalize to 0-255 range and convert back to uint8 (OpenCV standard) + # Clip values first to avoid overflow, then scale if needed + default_gray = np.clip(default_gray, 0, 255).astype(np.uint8) + custom_gray = np.clip(custom_gray, 0, 255).astype(np.uint8) + + return default_gray, custom_gray + +# -------------------------- Test the Code -------------------------- +if __name__ == "__main__": + # Replace with your image path + IMAGE_PATH = "./data/new_texture.jpg" + img = cv2.imread(IMAGE_PATH) + dx = -80 + dy = 0 + tile = img[410+dy:650+dy,940+dx:1180+dx,:] + + # Example 1: Use standard weights (verify correctness) + default_gray, red_gray = custom_gray_conversion( + tile, + r_weight=1, g_weight=0, b_weight=0 + ) + + default_gray, green_gray = custom_gray_conversion( + tile, + r_weight=0, g_weight=1, b_weight=0 + ) + default_gray, blue_gray = custom_gray_conversion( + tile, + r_weight=0, g_weight=0, b_weight=1 + ) + + edge_strength, edge_density = calculate_edge_strength_density(default_gray) + local_contrast = calculate_local_contrast(default_gray) + print(f"标准灰度: 边缘强度: {edge_strength}, 边缘密度: {edge_density}, 局部对比度: {local_contrast}") + edge_strength, edge_density = calculate_edge_strength_density(red_gray) + local_contrast = calculate_local_contrast(red_gray) + print(f"红色灰度: 边缘强度: {edge_strength}, 边缘密度: {edge_density}, 局部对比度: {local_contrast}") + edge_strength, edge_density = calculate_edge_strength_density(green_gray) + local_contrast = calculate_local_contrast(green_gray) + print(f"绿色灰度: 边缘强度: {edge_strength}, 边缘密度: {edge_density}, 局部对比度: {local_contrast}") + edge_strength, edge_density = calculate_edge_strength_density(blue_gray) + local_contrast = calculate_local_contrast(blue_gray) + print(f"蓝色灰度: 边缘强度: {edge_strength}, 边缘密度: {edge_density}, 局部对比度: {local_contrast}") + + plt.subplot(2, 2, 1) + plt.imshow(default_gray, cmap='gray') + plt.subplot(2, 2, 2) + plt.imshow(red_gray, cmap='gray') + plt.subplot(2, 2, 3) + plt.imshow(green_gray, cmap='gray') + plt.subplot(2, 2, 4) + plt.imshow(blue_gray, cmap='gray') + plt.show() diff --git a/src/orisys/Stitcher.py b/src/orisys/Stitcher.py index 88a2c47..599d5a8 100644 --- a/src/orisys/Stitcher.py +++ b/src/orisys/Stitcher.py @@ -446,7 +446,7 @@ class Stitcher: n_boxes = 0 bk_thresh = 0 # initial black threshold bk_thresh_step = 5 - bk_thresh_max = 100 + bk_thresh_max = 125 # Also include a low global V threshold to capture colored but dark tiles hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) @@ -503,7 +503,24 @@ class Stitcher: cropped_boxes.append((nx0, ny0, nx1, ny1)) # sort boxes left-to-right, top-to-bottom (useful for grid) - cropped_boxes = sorted(cropped_boxes, key=lambda b: (b[1], b[0])) + # Use quantized y-coordinates to group boxes into rows, making sorting robust to noise + if len(cropped_boxes) > 0: + # Calculate row tolerance based on typical box height (use median height) + heights = [b[3] - b[1] for b in cropped_boxes] + median_height = np.median(heights) if heights else target_h + # Use a fraction of the box height as tolerance (e.g., 20% of box height) + row_tolerance = max(1, int(median_height * 0.2)) + + # Quantize y-coordinates to group boxes into rows + def get_row_key(box): + y = box[1] # y0 coordinate + # Round y to nearest multiple of row_tolerance to group into rows + quantized_y = (y // row_tolerance) * row_tolerance + return (quantized_y, box[0]) # Sort by quantized row, then by x + + cropped_boxes = sorted(cropped_boxes, key=get_row_key) + else: + cropped_boxes = sorted(cropped_boxes, key=lambda b: (b[1], b[0])) self.boxes = cropped_boxes img_with_boxes = self.draw_boxes(img= img)