codekingpro/portable-devtools
115k
1import numpy as np2from PIL import Image3 4from fastembed.common.types import NumpyArray5 6 7def convert_to_rgb(image: Image.Image) -> Image.Image:8 if image.mode == "RGB":9 return image10 11 image = image.convert("RGB")12 return image13 14 15def center_crop(16 image: Image.Image | NumpyArray,17 size: tuple[int, int],18) -> NumpyArray:19 if isinstance(image, np.ndarray):20 _, orig_height, orig_width = image.shape21 else:22 orig_height, orig_width = image.height, image.width23 # (H, W, C) -> (C, H, W)24 image = np.array(image).transpose((2, 0, 1))25 26 crop_height, crop_width = size27 28 # left upper corner (0, 0)29 top = (orig_height - crop_height) // 230 bottom = top + crop_height31 left = (orig_width - crop_width) // 232 right = left + crop_width33 34 # Check if cropped area is within image boundaries35 if top >= 0 and bottom <= orig_height and left >= 0 and right <= orig_width:36 image = image[..., top:bottom, left:right]37 return image38 39 # Padding with zeros40 new_height = max(crop_height, orig_height)41 new_width = max(crop_width, orig_width)42 new_shape = image.shape[:-2] + (new_height, new_width)43 new_image = np.zeros_like(image, shape=new_shape, dtype=np.float32)44 45 top_pad = (new_height - orig_height) // 246 bottom_pad = top_pad + orig_height47 left_pad = (new_width - orig_width) // 248 right_pad = left_pad + orig_width49 new_image[..., top_pad:bottom_pad, left_pad:right_pad] = image50 51 top += top_pad52 bottom += top_pad53 left += left_pad54 right += left_pad55 56 new_image = new_image[57 ..., max(0, top) : min(new_height, bottom), max(0, left) : min(new_width, right)58 ]59 60 return new_image61 62 63def normalize(64 image: NumpyArray,65 mean: float | list[float],66 std: float | list[float],67) -> NumpyArray:68 num_channels = image.shape[1] if len(image.shape) == 4 else image.shape[0]69 70 if not np.issubdtype(image.dtype, np.floating):71 image = image.astype(np.float32)72 73 mean_list = mean if isinstance(mean, list) else [mean] * num_channels74 75 if len(mean_list) != num_channels:76 raise ValueError(77 f"mean must have the same number of channels as the image, image has {num_channels} channels, got "78 f"{len(mean_list)}"79 )80 81 mean_arr = np.array(mean_list, dtype=np.float32)82 83 std_list = std if isinstance(std, list) else [std] * num_channels84 if len(std_list) != num_channels:85 raise ValueError(86 f"std must have the same number of channels as the image, image has {num_channels} channels, got {len(std_list)}"87 )88 89 std_arr = np.array(std_list, dtype=np.float32)90 91 image_upd = ((image.T - mean_arr) / std_arr).T92 return image_upd93 94 95def resize(96 image: Image.Image,97 size: int | tuple[int, int],98 resample: int | Image.Resampling = Image.Resampling.BILINEAR,99) -> Image.Image:100 if isinstance(size, tuple):101 return image.resize(size, resample)102 103 height, width = image.height, image.width104 short, long = (width, height) if width <= height else (height, width)105 106 new_short, new_long = size, int(size * long / short)107 if width <= height:108 new_size = (new_short, new_long)109 else:110 new_size = (new_long, new_short)111 return image.resize(new_size, resample)112 113 114def rescale(image: NumpyArray, scale: float, dtype: type = np.float32) -> NumpyArray:115 return (image * scale).astype(dtype)116 117 118def pil2ndarray(image: Image.Image | NumpyArray) -> NumpyArray:119 if isinstance(image, Image.Image):120 return np.asarray(image).transpose((2, 0, 1))121 return image122 123 124def pad2square(125 image: Image.Image,126 size: int,127 fill_color: str | int | tuple[int, ...] = 0,128) -> Image.Image:129 height, width = image.height, image.width130 131 left, right = 0, width132 top, bottom = 0, height133 134 crop_required = False135 if width > size:136 left = (width - size) // 2137 right = left + size138 crop_required = True139 140 if height > size:141 top = (height - size) // 2142 bottom = top + size143 crop_required = True144 145 new_image = Image.new(mode="RGB", size=(size, size), color=fill_color)146 new_image.paste(image.crop((left, top, right, bottom)) if crop_required else image)147 return new_image148 149 150def resize_longest_edge(151 image: Image.Image,152 max_size: int,153 resample: int | Image.Resampling = Image.Resampling.LANCZOS,154) -> Image.Image:155 height, width = image.height, image.width156 aspect_ratio = width / height157 158 if width >= height:159 # Width is longer160 new_width = max_size161 new_height = int(new_width / aspect_ratio)162 else:163 # Height is longer164 new_height = max_size165 new_width = int(new_height * aspect_ratio)166 167 # Ensure even dimensions168 if new_height % 2 != 0:169 new_height += 1170 if new_width % 2 != 0:171 new_width += 1172 173 return image.resize((new_width, new_height), resample)174 175 176def crop_ndarray(177 image: NumpyArray,178 x1: int,179 y1: int,180 x2: int,181 y2: int,182 channel_first: bool = True,183) -> NumpyArray:184 if channel_first:185 # (C, H, W) format186 return image[:, y1:y2, x1:x2]187 else:188 # (H, W, C) format189 return image[y1:y2, x1:x2, :]190 191 192def resize_ndarray(193 image: NumpyArray,194 size: tuple[int, int],195 resample: int | Image.Resampling = Image.Resampling.LANCZOS,196 channel_first: bool = True,197) -> NumpyArray:198 # Convert to PIL-friendly format (H, W, C)199 if channel_first:200 img_hwc = image.transpose((1, 2, 0))201 else:202 img_hwc = image203 204 # Handle different dtypes205 if img_hwc.dtype == np.float32 or img_hwc.dtype == np.float64:206 # Assume normalized, scale to 0-255 for PIL207 img_hwc_scaled = (img_hwc * 255).astype(np.uint8)208 pil_img = Image.fromarray(img_hwc_scaled, mode="RGB")209 resized = pil_img.resize(size, resample)210 result = np.array(resized).astype(np.float32) / 255.0211 else:212 # uint8 or similar213 pil_img = Image.fromarray(img_hwc.astype(np.uint8), mode="RGB")214 resized = pil_img.resize(size, resample)215 result = np.array(resized)216 217 # Convert back to original format218 if channel_first:219 result = result.transpose((2, 0, 1))220 221 return result222 