Skip to content

Commit

Permalink
Merge pull request #62 from AllenNeuralDynamics/CICD_and_minor_updates
Browse files Browse the repository at this point in the history
CICD updates
  • Loading branch information
jsiegle authored May 1, 2024
2 parents 4cb48fd + 8f82b3d commit 5e2f9f7
Show file tree
Hide file tree
Showing 10 changed files with 237 additions and 93 deletions.
35 changes: 20 additions & 15 deletions .github/workflows/tag_and_publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,30 @@ on:

jobs:
build-and-release:
runs-on: windows-latest
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: '0' # Fetch all history to manage tags correctly

- name: Pull latest changes
run: git pull origin main
fetch-depth: '0'

- name: Set up Python
uses: actions/setup-python@v2 # Corrected from checkout to setup-python
uses: actions/setup-python@v2
with:
python-version: '3.8'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine build
- name: Install project
run: pip install -e . # Install your project in editable mode

- name: Extract version from the package
id: get_version
shell: bash
run: |
echo "RELEASE_VERSION=$(python -c 'import parallax; print(parallax.__version__)')" >> $GITHUB_ENV
RELEASE_VERSION=$(python -c 'import parallax; print(parallax.__version__)')
echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_ENV
echo "Extracted RELEASE_VERSION: $RELEASE_VERSION"
- name: Configure Git Account
run: |
git config --local user.email "[email protected]"
git config --local user.name "Hanna"
- name: Create Git tag
run: |
Expand All @@ -50,6 +47,14 @@ jobs:
draft: false
prerelease: true

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine build
- name: Install project
run: pip install -e .

- name: Build package
run: |
python -m build
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ To upgrate to the latest version:
pip install parallax-app --upgrade
```

3. To install camera interface:
```bash
pip install parallax-app[camera]
```

### Running Parallax
```bash
python -m parallax
Expand Down
8 changes: 8 additions & 0 deletions docs/ReadMe.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ To upgrate to the latest version:
pip install parallax-app --upgrade
3. To install camera interface:

.. code-block:: bash
pip install parallax-app[camera]
Running Parallax
=========================

Expand Down
2 changes: 1 addition & 1 deletion parallax/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import os

__version__ = "0.37.2"
__version__ = "0.37.5"

# allow multiple OpenMP instances
os.environ["KMP_DUPLICATE_LIB_OK"] = "True"
2 changes: 1 addition & 1 deletion parallax/calibration_camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

# Set logger name
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
logger.setLevel(logging.WARNING)
# Set the logging level for PyQt5.uic.uiparser/properties
logging.getLogger("PyQt5.uic.uiparser").setLevel(logging.DEBUG)
logging.getLogger("PyQt5.uic.properties").setLevel(logging.DEBUG)
Expand Down
128 changes: 100 additions & 28 deletions parallax/mask_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,61 @@
logging.getLogger("PyQt5.uic.uiparser").setLevel(logging.WARNING)
logging.getLogger("PyQt5.uic.properties").setLevel(logging.WARNING)


class MaskGenerator:
"""Class for generating a mask from an image."""

def __init__(self):
def __init__(self, initial_detect=False):
"""Initialize mask generator object"""
self.img = None
self.original_size = (None, None)
self.is_reticle_exist = True # TODO
self.is_reticle_exist = None
self.initial_detect = initial_detect

def _resize_and_blur(self):
"""Resize and blur the image."""
if len(self.img.shape) > 2:
self.img = cv2.cvtColor(self.img, cv2.COLOR_BGR2GRAY)
self.img = cv2.resize(self.img, (400, 300))
self.img = cv2.GaussianBlur(self.img, (9, 9), 0)
if self.initial_detect:
self.img = cv2.resize(self.img, (120, 90))
else:
self.img = cv2.resize(self.img, (400, 300))
self.img = cv2.GaussianBlur(self.img, (9, 9), 0)

def _apply_threshold(self):
"""Apply binary threshold to the image."""
_, self.img = cv2.threshold(
self.img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU
)

def _homomorphic_filter(self, gamma_high=1.5, gamma_low=0.5, c=1, d0=30):
# Apply the log transform
img_log = np.log1p(np.array(self.img, dtype="float"))

# Create a Gaussian highpass filter
rows, cols = img_log.shape
center_x, center_y = rows // 2, cols // 2
y, x = np.ogrid[:rows, :cols]
gaussian = np.exp(-c * ((x - center_x)**2 + (y - center_y)**2) / (2 * d0**2))
highpass = 1 - gaussian

# Apply the filter in the frequency domain
img_fft = np.fft.fft2(img_log)
img_fft = np.fft.fftshift(img_fft)
img_hp = img_fft * highpass

# Transform the image back to spatial domain
img_hp = np.fft.ifftshift(img_hp)
img_hp = np.fft.ifft2(img_hp)
img_hp = np.exp(np.real(img_hp)) - 1

# Normalize the image
img_hp = cv2.normalize(img_hp, None, alpha=0, beta=255, norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_8U)

# Apply gamma correction
img_gamma = img_hp.copy()
img_gamma = np.array(255 * (img_gamma / 255) ** gamma_high, dtype='uint8')
self.img = np.array(255 * (img_gamma / 255) ** gamma_low, dtype='uint8')

def _keep_largest_contour(self):
"""Keep the largest contour in the image."""
contours, _ = cv2.findContours(
Expand All @@ -53,7 +85,11 @@ def _keep_largest_contour(self):

def _apply_morphological_operations(self):
"""Apply morphological operations to the image."""
kernels = [cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (8, 8)),
if self.initial_detect:
kernels = [cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2, 2)),
cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))]
else:
kernels = [cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (8, 8)),
cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (10, 10))]

self.img = cv2.morphologyEx(self.img, cv2.MORPH_CLOSE, kernels[0])
Expand All @@ -71,36 +107,64 @@ def _remove_small_contours(self):
self.img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
for contour in contours:
if cv2.contourArea(contour) < 50 * 50:
self.img = cv2.drawContours(
self.img, [contour], -1, (0, 0, 0), -1
)
if self.initial_detect:
if cv2.contourArea(contour) < 5 * 5:
self.img = cv2.drawContours(
self.img, [contour], -1, (0, 0, 0), -1
)
else:
if cv2.contourArea(contour) < 50 * 50:
self.img = cv2.drawContours(
self.img, [contour], -1, (0, 0, 0), -1
)

def _finalize_image(self):
"""Resize the image back to its original size."""
self.img = cv2.resize(self.img, self.original_size)
self.img = cv2.convertScaleAbs(self.img)

def _is_reticle_frame(self):
def _is_reticle_frame(self, threshold = 0.5):
"""Check if the image contains a reticle frame.
Returns:
bool: True if the image contains a reticle frame, False otherwise.
"""
img = cv2.normalize(self.img, None, 0, 255, cv2.NORM_MINMAX)
img = img.astype(np.uint8)

hist = cv2.calcHist([img], [0], None, [255], [0, 255])
hist = cv2.GaussianBlur(hist, (91, 91), 0)
hist_smoothed = hist.squeeze()
peaks = np.where((hist_smoothed[:-2] < hist_smoothed[1:-1]) &
(hist_smoothed[1:-1] > hist_smoothed[2:]) &
(hist_smoothed[1:-1] > 300))[0] + 1

self.is_reticle_exist = True if len(peaks) >= 2 else False
logger.debug(f"is_reticle_exist: {self.is_reticle_exist}")
img_array = np.array(self.img)
boundary_depth = 5

# Extract boundary regions
top_boundary = img_array[:boundary_depth, :]
bottom_boundary = img_array[-boundary_depth:, :]
left_boundary = img_array[:, :boundary_depth]
right_boundary = img_array[:, -boundary_depth:]

# Calculate the total number of pixels in the boundary regions
total_boundary_pixels = 2 * (top_boundary.size + left_boundary.size)

# Black pixels are 0 in grayscale
white_pixel = 255

# Calculate black pixels in each boundary using boolean indexing
white_count = (
np.sum(top_boundary == white_pixel) +
np.sum(bottom_boundary == white_pixel) +
np.sum(left_boundary == white_pixel) +
np.sum(right_boundary == white_pixel)
)

# Determine if the percentage of black pixels is above the threshold
if (white_count / total_boundary_pixels) >= threshold:
self.is_reticle_exist = False
else:
if threshold == 0.5:
self.is_reticle_exist = True

return self.is_reticle_exist

def _reticle_exist_check(self, threshold):
if self.is_reticle_exist is None:
self._is_reticle_frame(threshold = threshold)

def process(self, img):
"""Process the input image and generate a mask.
Expand All @@ -120,12 +184,20 @@ def process(self, img):

self.img = img
self.original_size = img.shape[1], img.shape[0]
self._resize_and_blur()
if self.is_reticle_exist is None:
self._is_reticle_frame()
self._apply_threshold()
self._resize_and_blur() # Resize to smaller image and blur
if self.initial_detect:
self._homomorphic_filter() # Remove shadow

self._apply_threshold() # Global Thresholding
self._reticle_exist_check(threshold = 0.9)
if self.is_reticle_exist is False:
return None

self._keep_largest_contour()
self._apply_morphological_operations()
self._finalize_image() # Resize to original size
self._reticle_exist_check(threshold = 0.5)
if self.is_reticle_exist is False:
return None
self._finalize_image() # Resize back to original size

return self.img
20 changes: 14 additions & 6 deletions parallax/reticle_detect_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

# Set logger name
logger = logging.getLogger(__name__)
logger.setLevel(logging.WARNING)
logger.setLevel(logging.DEBUG)
# Set the logging level for PyQt5.uic.uiparser/properties to WARNING, to ignore DEBUG messages
logging.getLogger("PyQt5.uic.uiparser").setLevel(logging.DEBUG)
logging.getLogger("PyQt5.uic.properties").setLevel(logging.DEBUG)
Expand Down Expand Up @@ -51,7 +51,7 @@ def __init__(self, name):
self.IMG_SIZE_ORIGINAL = (4000, 3000) # TODO
self.frame_success = None

self.mask_detect = MaskGenerator()
self.mask_detect = MaskGenerator(initial_detect = True)
self.reticleDetector = ReticleDetection(
self.IMG_SIZE_ORIGINAL, self.mask_detect, self.name
)
Expand Down Expand Up @@ -166,20 +166,27 @@ def process(self, frame):
ret, frame_, _, inliner_lines_pixels = (
self.reticleDetector.get_coords(frame)
)
if ret:
if not ret:
logger.debug(f"{ self.name} get_coords fails ")
else:
ret, x_axis_coords, y_axis_coords = (
self.coordsInterests.get_coords_interest(
inliner_lines_pixels
)
)
if ret:

if not ret:
logger.debug(f"{ self.name} get_coords_interest fails ")
else:
# TODO
# ret, mtx, dist = self.calibrationCamera.get_predefined_intrinsic(x_axis_coords, y_axis_coords)
# if not ret:
ret, mtx, dist = self.calibrationCamera.calibrate_camera(
x_axis_coords, y_axis_coords
)
if ret:
if not ret:
logger.debug(f"{ self.name} calibrate_camera fails ")
else:
# Draw
self.found_coords.emit(
x_axis_coords, y_axis_coords, mtx, dist
Expand All @@ -189,11 +196,12 @@ def process(self, frame):
frame = self.draw(frame, x_axis_coords, y_axis_coords)
frame = self.draw_calibration_info(frame, ret, mtx, dist)
self.frame_success = frame

if self.frame_success is None:
logger.debug(f"{ self.name} reticle detection fail ")
return frame
else:
logger.debug(f"{ self.name} reticle detection success ")
logger.debug(f"{ self.name} reticle detection success \n")
return self.frame_success

def stop_running(self):
Expand Down
Loading

0 comments on commit 5e2f9f7

Please sign in to comment.