def __init__( self, images: List[ModuleOrPartialModuleImage], same_camera: bool = False, allow_different_dtypes=False, ): """Initialize a module image sequence Args: images (List[ModuleImage]): The list of images same_camera (bool): Indicates if all images are from the same camera allow_different_dtypes (bool): Allow images to have different datatypes? """ cols = images[0].cols rows = images[0].rows for img in images: if img.cols != cols: logging.error( "Cannot create sequence from different module configurations" ) exit() if img.rows != rows: logging.error( "Cannot create sequence from different module configurations" ) exit() super().__init__(images, same_camera, allow_different_dtypes)
def grid(self) -> np.ndarray: """Create a grid of corners according to the module geometry Returns: grid: (cols*rows, 2)-array of coordinates on a regular grid """ if self.cols is not None and self.rows is not None: x, y = np.mgrid[0:self.cols + 1:1, 0:self.rows + 1:1] grid = np.stack([x.flatten(), y.flatten()], axis=1) return grid else: logging.error("Module geometry is not initialized") exit()
def __init__( self, images: List[Image], same_camera: bool = False, allow_different_dtypes=False, ): """Initialize a module image sequence Args: images (List[Image]): The list of images came_camera (bool): Indicates, if all images are from the same camera and hence share the same intrinsic parameters allow_different_dtypes (bool): Allow images to have different datatypes? """ self._images = images self._same_camera = same_camera self._allow_different_dtypes = allow_different_dtypes self._meta_df = None if len(self.images) == 0: logging.error("Creation of an empty sequence is not supported") exit() # check that all have the same modality, dimension, dtype and module configuration shape = self.images[0].shape dtype = self.images[0].dtype modality = self.images[0].modality # for img in self.images: # if img.dtype != dtype and not allow_different_dtypes: # logging.error( # 'Cannot create sequence from mixed dtypes. Consider using the "allow_different_dtypes" argument, when reading images.' # ) # exit() # if img.shape != shape and same_camera: # logging.error( # 'Cannot create sequence from mixed shapes. Consider using the "same_camera" argument, when reading images.' # ) # exit() # if img.modality != modality: # logging.error("Cannot create a sequence from mixed modalities.") # exit() # namespace for accessing pandas methods self.pandas = self._PandasHandler(self)
def locate_module_and_cells( sequence: ModuleImageOrSequence, estimate_distortion: bool = True, orientation: str = None, return_bounding_boxes: bool = False, ) -> Union[Tuple[ModuleImageOrSequence, ObjectAnnotations], ModuleImageSequence]: """Locate a single module and its cells Note: This methods implements the following paper: Hoffmann, Mathis, et al. "Fast and robust detection of solar modules in electroluminescence images." International Conference on Computer Analysis of Images and Patterns. Springer, Cham, 2019. Args: sequence (ModuleImageOrSequence): A single module image or a sequence of module images estimate_distortion (bool): Set True to estimate lens distortion, else False orientation (str): Orientation of the module ('horizontal' or 'vertical' or None). If set to None (default), orientation is automatically determined return_bounding_boxes (bool): Indicates, if bounding boxes of returned modules are returned Returns: images: The same image/sequence with location information added """ if sequence[0].modality != EL_IMAGE: logging.error("Module localization is not supporting given imaging modality") exit() result = list() failures = 0 mcs = list() dts = list() flags = list() transforms = list() for img in tqdm(sequence.images): t, mc, dt, f = apply( img.data, img.cols, img.rows, is_module_detail=isinstance(img, PartialModuleImage), orientation=orientation, ) transforms.append(t) flags.append(f) mcs.append(mc) dts.append(dt) if estimate_distortion: if sequence.same_camera: # do joint estimation logging.info( "Jointly estimating parameters for lens distortion. This might take some time.." ) mcs_new = list() dts_new = list() valid = list() for mc, dt, f in zip(mcs, dts, flags): if mc is not None and dt is not None: mcs_new.append(mc[f]) dts_new.append(dt[f]) valid.append(True) else: valid.append(False) transforms = FullMultiTransform( mcs_new, dts_new, image_width=sequence.shape[1], image_height=sequence.shape[0], n_dist_coeff=1, ) transforms_new = list() i = 0 for v in valid: if v: transforms_new.append(transforms[i]) i += 1 else: transforms_new.append(None) transforms = transforms_new else: transforms = list() for mc, dt, f, img in zip(mcs, dts, flags, sequence.images): if mc is not None and dt is not None: t = FullTransform( mc[f], dt[f], image_width=img.shape[1], image_height=img.shape[0], n_dist_coeff=1, ) transforms.append(t) else: transforms.append(None) for t, img in zip(transforms, sequence.images): if t is not None and t.valid: img_res = type(img).from_other(img, meta={"transform": t}) result.append(img_res) else: result.append(deepcopy(img)) failures += 1 if failures > 0: logging.warning("Module localization falied for {:d} images".format(failures)) result = ModuleImageSequence.from_other(sequence, images=result) if not return_bounding_boxes: return result else: boxes = dict() # compute polygon for every module and accumulate results for img in result: if img.has_meta("transform"): c = img.cols r = img.rows coords = np.array([[0.0, 0.0], [c, 0.0], [c, r], [0.0, r]]) coords_transformed = img.get_meta("transform")(coords) poly = Polygon( [ (x, y) for x, y in zip( coords_transformed[:, 0].tolist(), coords_transformed[:, 1].tolist(), ) ] ) boxes[img.path.name] = [("Module", poly)] else: boxes[img.path.name] = [] return result, boxes
def segment_module_part( image: ModuleImage, first_col: int, first_row: int, cols: int, rows: int, size: int = None, padding: float = 0.0, ) -> PartialModuleImage: """Segment a part of a module Args: image (ModuleImage): The corresponding module image first_col (int): First column to appear in the segment first_row (int): First row to appear in the segment cols (int): Number of columns of the segment rows (int): Number of rows of the segment size (int): Size of a cell in pixels (automatically chosen by default) padding (float): Optional padding around the given segment relative to the cell size (must be in [0..1[ ) Returns: segment: The resulting segment """ if not image.has_meta("transform") or not image.get_meta("transform").valid: logging.error( "The ModuleImage does not have a valid transform. Did module localization succeed?" ) exit() t = image.get_meta("transform") if padding >= 1.0 or padding < 0.0: logging.error("padding needs to be in [0..1[") exit() last_col = first_col + cols last_row = first_row + rows size = t.mean_scale() if size is None else size result = warp_image( image.data, t, first_col - padding, first_row - padding, 1 / size, 1 / size, cols + 2 * padding, rows + 2 * padding, ) result = result.astype(image.data.dtype) transform = HomographyTransform( np.array( [ [first_col - padding, first_row - padding], [last_col + padding, first_row - padding], [last_col + padding, last_row + padding], [first_col - padding, last_row + padding], ] ), np.array( [ [0.0, 0.0], [result.shape[1], 0.0], [result.shape[1], result.shape[0]], [0.0, result.shape[0]], ] ), ) # bounding box in original image coords bb = [ [first_col - padding, first_row - padding], [first_col + cols + padding, first_row + rows + padding], ] bb = t(np.array(bb)) bb = Polygon.from_bounds(bb[0][0], bb[0][1], bb[1][0], bb[1][1]) original = image.from_other(image, meta={"segment_module_original_box": bb}) return PartialModuleImage.from_other( image, drop_meta_types=[Polygon], # geometric attributes are invalid now.. data=result, cols=cols + min(first_col, 0), rows=rows + min(first_row, 0), first_col=first_col if first_col >= 0 else None, first_row=first_row if first_row >= 0 else None, meta={"transform": transform, "segment_module_original": original}, )