Source code for concepts.gui.opencv_simple.point_picker

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
# File   : point_picker.py
# Author : Jiayuan Mao
# Email  : maojiayuan@gmail.com
# Date   : 02/21/2024
#
# This file is part of Project Concepts.
# Distributed under terms of the MIT license.

from typing import Optional, Tuple, List

import cv2
import numpy as np
import jacinle.io as io

__all__ = ['CV2PointPicker', 'cv2_point_picker']


[docs]class CV2PointPicker(object): """A simple point picker using OpenCV. Example: .. code-block:: python import cv2 import numpy as np from concepts.gui.opencv_simple.point_picker import CV2PointPicker color_image = np.zeros((480, 640, 3), dtype=np.uint8) depth_image = np.zeros((480, 640), dtype=np.uint16) picker = CV2PointPicker() points = picker.run(color_image, depth_image) The order of the points corresponds to the order in which they were clicked. To remove a point, click on it again. The current checking radius is 10 pixels. """
[docs] def __init__(self, registered_points: Optional[List[Tuple[int, int]]] = None): """Initialize the point picker. Args: registered_points: list of points to be registered. Defaults to None. """ if registered_points is None: self.registered_points = [] else: self.registered_points = list(registered_points)
[docs] def register_point(self, event, x, y, flag, param): """Register a point based on the click by the user. See OpenCV documentation for :meth:`cv2.setMouseCallback` for more details. Args: event: the event type. x: the x-coordinate of the click. y: the y-coordinate of the click. flag: the flag. param: the parameter. """ if event == cv2.EVENT_LBUTTONDOWN: found = None for i, (xx, yy) in enumerate(self.registered_points): if np.linalg.norm(np.array([xx, yy], dtype='float32') - np.array([x, y], dtype='float32')) < 10: found = i break if found is not None: self.registered_points = self.registered_points[:found] + self.registered_points[found + 1:] else: self.registered_points.append((x, y))
[docs] def imshow(self, color_image: np.ndarray, depth_image: Optional[np.ndarray], window_name: Optional[str] = None) -> None: """Helper function to visualize the registered points on the color and depth images. Args: color_image: the color image. depth_image: the depth image. window_name: the name of the window. Defaults to None, in which case the name of the class is used. """ color_image_new = color_image.copy() for x, y in self.registered_points: cv2.circle(color_image_new, (x, y), 5, (255, 0, 0), -1) if depth_image is None: # Form heatmap for depth and stack with color image depth_colormap = cv2.applyColorMap( cv2.convertScaleAbs(depth_image, alpha=0.03), cv2.COLORMAP_JET ) concat_images = np.hstack((color_image_new, depth_colormap)) else: concat_images = color_image_new if window_name is None: window_name = str(self) cv2.namedWindow(window_name, cv2.WINDOW_NORMAL) cv2.imshow(window_name, concat_images) cv2.setMouseCallback(window_name, self.register_point) cv2.waitKey(1)
[docs] def run( self, color_image: np.ndarray, depth_image: Optional[np.ndarray] = None, file_dirname: str = 'visualization', file_prefix: str = '', save: bool = False, window_name: Optional[str] = None ) -> List[Tuple[int, int]]: """Run the point picker. - To pick a point, click on the image. - To remove a point, click on it again. - Press 'c' to print the registered points, and save it to a file if `save` is True. - Press 'esc' or 'q' to exit. The function will return the registered points. If `save` is True, the visualization and the points will be saved to the specified directory: `{file_dirname}/{file_prefix}_visualization.png` and `{file_dirname}/{file_prefix}_points.pkl`. Args: color_image: the color image. depth_image: the depth image. Defaults to None. If specified, it will be shown side by side with the color image. file_dirname: the directory to save the visualization and the points. Defaults to 'visualization'. file_prefix: the prefix for the file names. Defaults to ''. save: whether to save the visualization and the points. Defaults to False. window_name: the name of the window. Defaults to None, in which case the name of the class is used. Returns: list of registered points. """ depth_colormap = None if depth_image is not None: depth_colormap = cv2.applyColorMap( cv2.convertScaleAbs(depth_image, alpha=0.03), cv2.COLORMAP_JET ) if window_name is None: window_name = str(self) while True: color_image_new = color_image.copy() for x, y in self.registered_points: cv2.circle(color_image_new, (x, y), 5, (255, 0, 0), -1) # Form heatmap for depth and stack with color image if depth_image is not None: concat_images = np.hstack((color_image_new, depth_colormap)) else: concat_images = color_image_new cv2.namedWindow(window_name, cv2.WINDOW_NORMAL) cv2.imshow(window_name, concat_images) cv2.setMouseCallback(window_name, self.register_point) key = cv2.waitKey(1) # Exit on 'esc' or 'q' if key & 0xFF == ord("q") or key == 27: cv2.destroyWindow(window_name) break if key == ord('c'): print('Registered points:') print('-' * 80) print(self.registered_points) if save: cv2.imwrite(f'{file_dirname}/{file_prefix}visualization.png', concat_images) io.dump(f'{file_dirname}/{file_prefix}points.pkl', self.registered_points) if save: cv2.imwrite(f'{file_dirname}/{file_prefix}visualization.png', concat_images) io.dump(f'{file_dirname}/{file_prefix}points.pkl', self.registered_points) cv2.destroyAllWindows() return self.registered_points
[docs]def cv2_point_picker(color_image: np.ndarray, depth_image: Optional[np.ndarray] = None, **kwargs) -> List[Tuple[int, int]]: """A simple point picker using OpenCV. See :class:`CV2PointPicker` for more details. Args: color_image: the color image. depth_image: the depth image. Defaults to None. If specified, it will be shown side by side with the color image. kwargs: additional keyword arguments. Returns: list of registered points. """ picker = CV2PointPicker() return picker.run(color_image, depth_image, **kwargs)