Skip to content

Commit

Permalink
Add build system for modern OS versions (#8)
Browse files Browse the repository at this point in the history
* Modernise deps and build system

* Builds

* Builds

* Builds

* Builds

* Bump to py 3.12

* Use spec files

* Dir

* Dir

* Fix pathing

* Try plisting

* Try plisting

* Try plisting

* Try onefile

* Try py2app

* Try py2app

* Pyinstaller w/ tar

* Pyinstaller w/ tar

* Pyinstaller w/ tar

* Pyinstaller w/ tar

* Onedir, not onefile

* Cleanup

* Cleanup

* Fix file dialogs, cleanup
  • Loading branch information
DavidStirling authored Jun 5, 2024
1 parent 6a2fbba commit 5808424
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 19 deletions.
53 changes: 53 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Build
on:
push:
pull_request:

jobs:
build:
name: Build
strategy:
matrix:
os: [ macos-13, windows-latest ]
include:
- os: macos-13
spec: build_macos.spec
package_name: macos-binary
- os: windows-latest
spec: build_windows.spec
package_name: windows-binary
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- name: Install deps
run: |
python -m pip install setuptools wheel pyinstaller
python -m pip install -r requirements.txt
python -m pip list
- name: Create Executable
run: pyinstaller ${{ matrix.spec }}
- name: Compress Executable
if: matrix.os == 'macos-13'
working-directory: ./dist
run: |
tar -zcvf QuantiFish.tar.gz QuantiFish.app
- name: Artifact upload
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.package_name }}
path: |
dist/QuantiFish.exe
dist/QuantiFish.tar.gz
retention-days: 30
- name: Create release
if: startsWith(github.ref, 'refs/tags')
uses: softprops/action-gh-release@v1
with:
files: |
./dist/*.exe
./dist/*.app
draft: true
6 changes: 5 additions & 1 deletion ChangeLog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,8 @@ Version 2.1 - 07/04/2019

Version 2.1.1 - 07/05/2019
- Rename "Clusters" to "Foci".
- Fix error with grid analysis when trying to use box sizes larger than the image.
- Fix error with grid analysis when trying to use box sizes larger than the image.

Version 2.1.2 - 04/06/2024
- Support modern windows/macos versions.
- Proper build system with GH actions.
46 changes: 28 additions & 18 deletions QuantiFish.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# QuantiFish - A tool for quantification of fluorescence in Zebrafish embryos.
# Copyright(C) 2017-2019 David Stirling
# Copyright(C) 2017-2024 David Stirling

"""This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
Expand Down Expand Up @@ -32,7 +32,7 @@
from skimage.measure import regionprops, label
from skimage.transform import rescale

version = "2.1.1"
version = "2.1.2"


# Get path for unpacked Pyinstaller exe (MEIPASS), else default to current directory.
Expand Down Expand Up @@ -112,10 +112,13 @@ def __init__(self, master):
# Header Bar
self.img = ImageTk.PhotoImage(Image.open(resource_path("resources/QFLogo")))
self.logo = ttk.Label(self.header, image=self.img)
self.title = ttk.Label(self.header, text="QuantiFish ", font=("Arial", 25),
justify=tk.CENTER).grid(column=2, columnspan=1, row=1, sticky=tk.E + tk.W)
self.subtitle = ttk.Label(self.header, text="Zebrafish Image Analyser", font=("Arial", 10),
justify=tk.CENTER).grid(column=2, columnspan=1, row=2, sticky=tk.E + tk.W)
ttk.Label(self.header, text="QuantiFish ", font=("Arial", 25),
justify=tk.CENTER).grid(column=2, columnspan=1, row=1,
sticky=tk.E + tk.W)
ttk.Label(self.header, text="Zebrafish Image Analyser",
font=("Arial", 10),
justify=tk.CENTER).grid(column=2, columnspan=1, row=2,
sticky=tk.E + tk.W)
self.about = ttk.Button(self.header, text="About", command=self.about)
self.logo.grid(column=1, row=1, rowspan=2, sticky=tk.W)
self.about.grid(column=4, row=1, rowspan=1, sticky=tk.E, padx=10, pady=5)
Expand Down Expand Up @@ -399,7 +402,8 @@ def preview_filelist(self):

else:
self.logevent("No image directory set, unable to generate file list.")
self.filelist_contents.filelistlabel.config(text="0 files to be analysed")
if self.filelist_contents:
self.filelist_contents.filelistlabel.config(text="0 files to be analysed")

# Run file scan on another thread.
def filelist_thread(self):
Expand All @@ -409,7 +413,7 @@ def filelist_thread(self):
time.sleep(0.5)
self.list_stopper.set()
filegenthread = threading.Thread(target=self.preview_filelist, args=())
filegenthread.setDaemon(True)
filegenthread.daemon = True
filegenthread.start()

# Close file list window.
Expand Down Expand Up @@ -625,7 +629,7 @@ def previewclusters(imgarray, minimumarea):
clusterim[clustermask] = (0, 75, 255)
if clusterim.shape[1] > 750:
self.resizefactor = 750 / clusterim.shape[1]
clusterim = rescale(clusterim, self.resizefactor)
clusterim = rescale(clusterim, self.resizefactor, channel_axis=-1 if len(clusterim.shape) > 2 else None)
clusterim = (clusterim * 255).astype('uint8')
return clusterim

Expand All @@ -642,7 +646,7 @@ def previewclusters(imgarray, minimumarea):
# Reduce preview spawner to 8-bit range
if imfile.shape[1] > 750:
self.resizefactor = 750 / im.shape[1]
self.imsml = rescale(im, self.resizefactor)
self.imsml = rescale(im, self.resizefactor, channel_axis=-1 if len(im.shape) > 2 else None)
self.imsml = (self.imsml * 255).astype('uint8')
else:
self.imsml = im
Expand Down Expand Up @@ -719,8 +723,9 @@ def headers(self):

# Exports data to csv file
def datawriter(self, exportpath, exportdata):
writeme = (exportpath,) + exportdata + (self.threshold.get(), self.threshold.get() * app.scalemultiplier,
app.currentchannel)
writeme = [exportpath, *exportdata, self.threshold.get(),
self.threshold.get() * app.scalemultiplier,
app.currentchannel]
try:
savefile = self.savedir.get() + '/' + self.savefilename.get() + '.csv'
with open(savefile, 'a', newline="\n", encoding="utf-8") as f:
Expand Down Expand Up @@ -778,7 +783,7 @@ def runscript(self):
mprokilla = threading.Event()
mprokilla.set()
mpro = threading.Thread(target=cyclefiles, args=(mprokilla, self.directory.get()))
mpro.setDaemon(True)
mpro.daemon = True
mpro.start()
except (AttributeError, ValueError, TypeError, OSError, PermissionError, IOError):
app.logevent("Error initiating script, aborting")
Expand Down Expand Up @@ -812,7 +817,7 @@ def ui_lock(self):
self.runbutton.config(text="Run", command=app.runscript)
self.locked = False
else:
for widget in listwidgets + manualwidgets:
for widget in (*listwidgets, *manualwidgets):
widget.state(['disabled'])
self.threslide.config(state=tk.DISABLED)
self.runbutton.config(text="Stop", command=app.abort)
Expand Down Expand Up @@ -1017,7 +1022,9 @@ def genstats(inputimage, threshold, wantclusters, file):
# Cluster Analysis
def getclusters(trgtimg, threshold, minimumarea, file):
# Find and count peaks above threshold, assign labels to clusters of stainng.
localmax = peak_local_max(trgtimg, indices=False, threshold_abs=threshold)
peaks = peak_local_max(trgtimg, threshold_abs=threshold)
localmax = np.zeros_like(trgtimg, dtype=bool)
localmax[tuple(peaks.T)] = True
peaks, numpeaks = label(localmax, return_num=True)
simpleclusters, numclusters = label(trgtimg > 0, return_num=True)
# Create table of cluster ids vs size of each, then list clusters bigger than minsize
Expand All @@ -1031,7 +1038,10 @@ def getclusters(trgtimg, threshold, minimumarea, file):
filthresholded[np.invert(clustermask)] = 0
intintfil = np.sum(filthresholded)
countfil = np.count_nonzero(filthresholded)
localmax2 = peak_local_max(filthresholded[:, :], indices=False, threshold_abs=threshold)
peaks2 = peak_local_max(filthresholded[:, :], threshold_abs=threshold)
localmax2 = np.zeros_like(filthresholded[:, :], dtype=bool)
localmax2[tuple(peaks2.T)] = True

targetpeaks, numtargetpeaks = label(localmax2, return_num=True)
cprops = regionprops(simpleclusters, trgtimg) # Get stats for each cluster
currentid = 0
Expand Down Expand Up @@ -1349,9 +1359,9 @@ def __init__(self, master):
self.heading.pack()
self.line2 = tk.Label(self.aboutwindow, text="Version " + version, font=("Consolas", 10), justify=tk.CENTER)
self.line2.pack(pady=(0, 5))
self.line3 = tk.Label(self.aboutwindow, text="David Stirling, 2017-2019", font=("Arial", 10), justify=tk.CENTER)
self.line3 = tk.Label(self.aboutwindow, text="David Stirling, 2017-2024", font=("Arial", 10), justify=tk.CENTER)
self.line3.pack()
self.line4 = tk.Label(self.aboutwindow, text="@DavidRStirling", font=("Arial", 10), justify=tk.CENTER)
self.line4 = tk.Label(self.aboutwindow, text="github.com/DavidStirling", font=("Arial", 10), justify=tk.CENTER)
self.line4.pack(pady=(0, 5))
self.aboutwindow.pack()

Expand Down
56 changes: 56 additions & 0 deletions build_macos.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# -*- mode: python ; coding: utf-8 -*-

a = Analysis(
['QuantiFish.py'],
pathex=[],
binaries=[],
datas=[('resources', 'resources')],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)

pyz = PYZ(a.pure)

exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='QuantiFish',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
icon=['resources/QFIcon.icns'],
)

coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='QuantiFish',
)

app = BUNDLE(
coll,
name='QuantiFish.app',
icon='resources/QFIcon.icns',
info_plist={
'CFBundleVersion': '2.1.2',
'CFBundleShortVersionString': '2.1.2',
'NSPrincipalClass': 'NSApplication',
},
)
45 changes: 45 additions & 0 deletions build_windows.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# -*- mode: python ; coding: utf-8 -*-


a = Analysis(
['QuantiFish.py'],
pathex=[],
binaries=[],
datas=[('resources', 'resources')],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)

exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='quantifish',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=['resources/QFIcon.ico'],
)
app = BUNDLE(
exe,
name='QuantiFish.exe',
icon='resources/QFIcon.ico',
bundle_identifier=None,
)
20 changes: 20 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from setuptools import setup
import os

if os.name == 'nt':
OPTIONS = {}
EXTRAS = []
else:
OPTIONS = {'py2app': {
'resources': './resources',
'iconfile': f'./resources/QFIcon.icns'
}}
EXTRAS = ['py2app']


setup(
app=['QuantiFish.py'],
options=OPTIONS,
setup_requires=EXTRAS,
install_requires=["scikit-image", "scipy", "pillow", "numpy"],
)

0 comments on commit 5808424

Please sign in to comment.