Skip to content
Closed

Dev #58

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
bd44307
Fix FileDirValue to validate input before setting value
AdenAthar Sep 11, 2025
9d1e5c3
Add GitHub Actions CI/CD with pytest coverage and QLTY Cloud integration
AdenAthar Nov 17, 2025
4c36602
Add pandas to dependencies
AdenAthar Nov 17, 2025
a2e4f48
Add debug step and fix QLTY coverage file path
AdenAthar Nov 17, 2025
7290a55
Create test.txt
AdenAthar Nov 18, 2025
75fcdac
Delete test.txt
AdenAthar Nov 18, 2025
a7c454f
Clean up CI workflow - remove debug step and test branch trigger
AdenAthar Nov 20, 2025
7740011
Merge pull request #45 from SystemsGenetics/pr-testing
AdenAthar Nov 21, 2025
f670ae4
Add QR code detection for variety identification
AdenAthar Dec 2, 2025
b5ba85d
Add QR code generator script for tray labels
AdenAthar Dec 13, 2025
1fdd30e
Refactor starch scales to load from YAML configuration
AdenAthar Dec 15, 2025
f8e8689
Merge pull request #46 from SystemsGenetics/yaml-starch-scales
AdenAthar Dec 15, 2025
4a3bc17
Refactor starch scales to load from YAML asset file
AdenAthar Dec 15, 2025
46ea3e8
Merge branch 'dev' into yaml-starch-scales
AdenAthar Dec 16, 2025
34d4873
Merge pull request #47 from SystemsGenetics/yaml-starch-scales
AdenAthar Dec 16, 2025
44a715e
Add QR code integration for variety tracking
AdenAthar Dec 18, 2025
1241b12
Remove QR code generator script
AdenAthar Dec 19, 2025
ac390fa
Add 8 new starch scale varieties and configurable text color
AdenAthar Jan 5, 2026
bf790ef
Update PURDUE to default threshold 172, keep threshold 150 as comment
AdenAthar Jan 7, 2026
1c74e03
Add results/ directory to gitignore
AdenAthar Jan 21, 2026
5548a78
Implement percentage-based threshold for starch detection
AdenAthar Jan 22, 2026
b15b5bc
Update 8 starch scale varieties with transparent-background calibration
AdenAthar Jan 23, 2026
8c57547
Add barcode detection support using pyzbar
AdenAthar Jan 23, 2026
2d4764e
Merge pull request #48 from SystemsGenetics/yaml-starch-scales
AdenAthar Jan 29, 2026
1807b33
Refactor QR metadata handling into base Analysis class
AdenAthar Jan 29, 2026
d645903
Add venv and coverage files to gitignore
AdenAthar Jan 29, 2026
5b94e6a
Add rotation-invariant barcode detection and QR metadata to tray summary
AdenAthar Jan 29, 2026
eddeadd
Add functional tests for QR detector, metadata parsing, and tray summary
AdenAthar Feb 2, 2026
8a6651a
Add 4 new MSU starch scale varieties (EMPIRE, FUJI, IDARED, JONATHAN)
AdenAthar Feb 12, 2026
cf74199
Change default starch threshold from 172 to 140 (55%)
AdenAthar Feb 12, 2026
e8ada6f
Merge starch-percentage-calculation into starch-scales-55-percent
AdenAthar Feb 12, 2026
8e6edbb
Update starch scales with 55% threshold calibration data
AdenAthar Feb 12, 2026
d62ff0d
Remove CLAUDE.md from gitignore
AdenAthar Feb 13, 2026
60d6853
Merge branch 'dev' into qr-code-detection
AdenAthar Feb 13, 2026
50db60c
Merge pull request #50 from SystemsGenetics/qr-code-detection
AdenAthar Feb 13, 2026
680ab0f
Merge pull request #51 from SystemsGenetics/functional-testing
AdenAthar Feb 13, 2026
c5c2741
Merge pull request #52 from SystemsGenetics/add-tests
AdenAthar Feb 13, 2026
6a40abc
Fix Developer's Guide documentation to match actual codebase
AdenAthar Feb 23, 2026
ed73ff3
Merge pull request #53 from SystemsGenetics/fix-developer-guide-docs
AdenAthar Feb 23, 2026
90171a3
Rename starch calculation variables for clarity, remove duplicate HON…
AdenAthar Feb 25, 2026
16edc46
Remove unused nested functions, inline histogram logic in _calculateS…
AdenAthar Feb 25, 2026
e4f7814
Update starch scales for Cornell, Purdue, Danjou, WA38 with 55% thres…
AdenAthar Feb 25, 2026
0d20e9e
Merge pull request #54 from SystemsGenetics/starch-scales-55-percent
AdenAthar Feb 25, 2026
88cd50b
Update starch scales: add ENZA, new WA38_1/WA38_2, remove old entries
AdenAthar Feb 27, 2026
f3f9161
Merge pull request #55 from SystemsGenetics/starch-scales-55-percent
AdenAthar Feb 27, 2026
0d8e24a
Add comprehensive unit tests for StarchArea
AdenAthar Mar 6, 2026
0d0bfa6
Merge pull request #56 from SystemsGenetics/starch-tests
AdenAthar Mar 6, 2026
09b8497
Increase default segmentation confidence threshold to 0.7
AdenAthar Mar 11, 2026
fafb648
Merge pull request #57 from SystemsGenetics/increase-conf-threshold
AdenAthar Mar 11, 2026
c50a9e9
Remove duplicate starch_scales.yml from config directory
AdenAthar Mar 26, 2026
9d8fc79
Expand test suite from 92 to 156 tests
AdenAthar Apr 3, 2026
2b4b9c0
Clean up .gitignore: remove duplicate *.csv, add venv/ and coverage a…
AdenAthar Apr 24, 2026
bcd9cf2
Update Pipfile python_version from 3.9 to 3.12 to match CI matrix
AdenAthar Apr 24, 2026
7304238
Add version pins to dependencies, move pytest to dev extras
AdenAthar Apr 24, 2026
3a76f58
Add integration tests for StarchArea and BlushColor pipelines
AdenAthar Apr 24, 2026
e7bdfb9
Merge branch 'dev' into fix-gitignore
AdenAthar Apr 27, 2026
b8c8148
Merge pull request #60 from SystemsGenetics/fix-gitignore
AdenAthar Apr 27, 2026
52fdf1e
Merge pull request #61 from SystemsGenetics/fix-pipfile-python
AdenAthar Apr 27, 2026
ff569e1
Merge branch 'dev' into dependency-pins
AdenAthar Apr 27, 2026
ce678d4
Merge pull request #62 from SystemsGenetics/dependency-pins
AdenAthar Apr 27, 2026
5464fa6
Merge pull request #59 from SystemsGenetics/add-more-tests
AdenAthar Apr 27, 2026
88c44d2
Merge pull request #63 from SystemsGenetics/integration-tests
AdenAthar Apr 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/pytest-coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Pytest with Coverage

on:
pull_request:
branches:
- dev
- main
push:
branches:
- dev
- main

permissions:
contents: read
id-token: write

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov
pip install -e .

- name: Run pytest with coverage
run: |
pytest --cov=Granny --cov-report=lcov:coverage.lcov --cov-report=term-missing

- name: Upload coverage to QLTY Cloud
if: matrix.python-version == '3.12'
uses: qltysh/qlty-action/coverage@v1
with:
oidc: true
files: coverage.lcov
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ dist/
06-Package
*logs/
_build/
venv/
results/

# Files
*.pyc
Expand All @@ -26,7 +28,6 @@ output.out
*.sh
*.zip
test_perf.py
*.csv
*.onnx
*.pt
*.lock
Expand All @@ -38,3 +39,5 @@ test_perf.py
*.jpg
*.tiff
CLAUDE.md
.coverage
coverage.lcov
76 changes: 76 additions & 0 deletions Granny/Analyses/Analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,82 @@ def resetRetValues(self):
"""
self.ret_values = {}

def _parse_qr_from_filename(self, filename: str) -> dict:
"""
Extract QR code information from segmented image filename.

Expected format: PROJECT_LOT_DATE_VARIETY_fruit_##.png
Example: APPLE2025_LOT001_2025-12-02_BB-Late_fruit_01.png

Args:
filename: Image filename (with or without path)

Returns:
Dictionary with QR information:
{
'project': project code or empty string,
'lot': lot code or empty string,
'date': date string or empty string,
'variety': variety string or empty string
}

Notes:
- Returns empty strings for all fields if parsing fails
- Handles legacy filenames gracefully (no QR data)
"""
import re
from pathlib import Path

# Extract just the filename without path
filename_only = Path(filename).name

# Pattern: PROJECT_LOT_DATE_VARIETY_fruit_##.png
# Use regex to match everything before "_fruit_##"
pattern = r'^(.+?)_(.+?)_(.+?)_(.+?)_fruit_\d+\.(?:png|jpg|jpeg)$'
match = re.match(pattern, filename_only)

if match:
return {
'project': match.group(1),
'lot': match.group(2),
'date': match.group(3),
'variety': match.group(4)
}
else:
# Parsing failed - return empty strings (no QR data)
return {
'project': '',
'lot': '',
'date': '',
'variety': ''
}

def _add_qr_metadata(self, result_img, filename: str):
"""
Parse QR/barcode metadata from filename and add to result image.

Args:
result_img: Image instance to add metadata values to
filename: Image filename to parse
"""
qr_info = self._parse_qr_from_filename(filename)
if qr_info['project']:
project_val = StringValue("project", "project", "Project code from QR code")
project_val.setValue(qr_info['project'])
result_img.addValue(project_val)

lot_val = StringValue("lot", "lot", "Lot code from QR code")
lot_val.setValue(qr_info['lot'])
result_img.addValue(lot_val)

date_val = StringValue("date", "date", "Date from QR code")
date_val.setValue(qr_info['date'])
result_img.addValue(date_val)

variety_val = StringValue("variety", "variety", "Variety from QR code")
variety_val.setValue(qr_info['variety'])
result_img.addValue(variety_val)

def performAnalysis(self) -> List[Image]:
"""
Once all required parameters have been set, this function is used
Expand Down
4 changes: 4 additions & 0 deletions Granny/Analyses/BlushColor.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from Granny.Models.Values.ImageListValue import ImageListValue
from Granny.Models.Values.IntValue import IntValue
from Granny.Models.Values.MetaDataValue import MetaDataValue
from Granny.Models.Values.StringValue import StringValue
from numpy.typing import NDArray


Expand Down Expand Up @@ -266,6 +267,9 @@ def _processImage(self, image_instance: Image) -> Image:
result_img: Image = RGBImage(image_instance.getImageName())
result_img.setImage(result)

# Extract and add QR/barcode metadata from filename (if present)
self._add_qr_metadata(result_img, image_instance.getImageName())

# saves the calculated score to the image_instance as a parameter
rating = FloatValue(
"rating", "rating", "Granny calculated rating of total blush area."
Expand Down
4 changes: 4 additions & 0 deletions Granny/Analyses/PeelColor.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from Granny.Models.Values.ImageListValue import ImageListValue
from Granny.Models.Values.IntValue import IntValue
from Granny.Models.Values.MetaDataValue import MetaDataValue
from Granny.Models.Values.StringValue import StringValue
from numpy.typing import NDArray


Expand Down Expand Up @@ -493,6 +494,9 @@ def _processImage(self, image_instance: Image) -> Image:
)
b_value.setValue(b)

# Extract and add QR/barcode metadata from filename (if present)
self._add_qr_metadata(image_instance, image_instance.getImageName())

# adds ratings to to the image_instance as parameters
image_instance.addValue(
bin_value,
Expand Down
73 changes: 70 additions & 3 deletions Granny/Analyses/Segmentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from Granny.Models.Values.FloatValue import FloatValue
from Granny.Models.Values.ImageListValue import ImageListValue
from Granny.Models.Values.IntValue import IntValue
from Granny.Utils.QRCodeDetector import QRCodeDetector
from numpy.typing import NDArray


Expand Down Expand Up @@ -142,7 +143,7 @@ def __init__(self):
)
self.conf_threshold.setMin(0.0)
self.conf_threshold.setMax(1.0)
self.conf_threshold.setValue(0.25)
self.conf_threshold.setValue(0.7)
self.conf_threshold.setIsRequired(False)

# YOLO IOU threshold parameter
Expand Down Expand Up @@ -211,6 +212,36 @@ def __init__(self):
self.text_thickness.setValue(3)
self.text_thickness.setIsRequired(False)

self.text_color_r = IntValue(
"text_color_r",
"text_color_r",
"Red channel value for text color (0-255). Default is 0 (black).",
)
self.text_color_r.setMin(0)
self.text_color_r.setMax(255)
self.text_color_r.setValue(0)
self.text_color_r.setIsRequired(False)

self.text_color_g = IntValue(
"text_color_g",
"text_color_g",
"Green channel value for text color (0-255). Default is 0 (black).",
)
self.text_color_g.setMin(0)
self.text_color_g.setMax(255)
self.text_color_g.setValue(0)
self.text_color_g.setIsRequired(False)

self.text_color_b = IntValue(
"text_color_b",
"text_color_b",
"Blue channel value for text color (0-255). Default is 0 (black).",
)
self.text_color_b.setMin(0)
self.text_color_b.setMax(255)
self.text_color_b.setValue(0)
self.text_color_b.setIsRequired(False)

# Sorting/grouping parameter
self.row_tolerance = IntValue(
"row_tolerance",
Expand Down Expand Up @@ -267,6 +298,10 @@ def __init__(self):
)
)

# Initialize QR code detector for variety information extraction
self.qr_detector = QRCodeDetector()
self.variety_info = None # Will store detected variety information if QR code found

self.addInParam(
self.model,
self.input_images,
Expand All @@ -277,6 +312,9 @@ def __init__(self):
self.bbox_thickness,
self.font_scale,
self.text_thickness,
self.text_color_r,
self.text_color_g,
self.text_color_b,
self.row_tolerance,
)

Expand Down Expand Up @@ -388,7 +426,7 @@ def _extractMaskedImage(self, tray_image: Image) -> Image:
(x1, y1),
fontFace=cv2.FONT_HERSHEY_SIMPLEX,
fontScale=self.font_scale.getValue(),
color=(255, 255, 255),
color=(self.text_color_b.getValue(), self.text_color_g.getValue(), self.text_color_r.getValue()),
thickness=self.text_thickness.getValue(),
)
image_instance: Image = RGBImage(
Expand Down Expand Up @@ -542,7 +580,19 @@ def _extractImage(self, tray_image: Image) -> List[Image]:
mask = sorted_masks[i]
for channel in range(3):
individual_image[:, :, channel] = tray_image_array[y1:y2, x1:x2, channel] * mask[y1:y2, x1:x2] # type: ignore
image_name = pathlib.Path(tray_image.getImageName()).stem + f"_fruit_{i+1:02d}" + ".png"

# Build filename: use QR data if detected, otherwise use default tray name
if self.variety_info is not None:
# QR code detected - use PROJECT_LOT_DATE_VARIETY_fruit_##.png
project = self.variety_info['project']
lot = self.variety_info['lot']
date = self.variety_info['date']
variety = self.variety_info['full']
image_name = f"{project}_{lot}_{date}_{variety}_fruit_{i+1:02d}.png"
else:
# No QR code - use default naming: tray_name_fruit_##.png
image_name = pathlib.Path(tray_image.getImageName()).stem + f"_fruit_{i+1:02d}" + ".png"

image_instance: Image = RGBImage(image_name)
image_instance.setImage(individual_image)
individual_images.append(image_instance)
Expand Down Expand Up @@ -610,6 +660,22 @@ def performAnalysis(self) -> List[Image]:
if h > w:
image_instance.rotateImage()

# Detect QR code to extract variety information (optional)
try:
qr_data, qr_points = self.qr_detector.detect(image_instance.getImage())
if qr_data:
self.variety_info = self.qr_detector.extract_variety_info(qr_data)
print(f"QR Code detected: {qr_data}")
print(f" Project: {self.variety_info['project']}, Lot: {self.variety_info['lot']}")
print(f" Date: {self.variety_info['date']}, Variety: {self.variety_info['full']}")
else:
print("No QR code detected - using default naming")
self.variety_info = None
except Exception as e:
# QR detection failed, continue with default naming
print(f"QR detection error: {str(e)} - using default naming")
self.variety_info = None

# predicts fruit instances in the image
result = self._segmentInstances(image=image_instance.getImage())

Expand All @@ -636,6 +702,7 @@ def performAnalysis(self) -> List[Image]:

self.masked_images.setImageList([masked_image])
self.masked_images.writeValue()

except:
AttributeError("Error with the results.")

Expand Down
Loading
Loading