Skip to content

Commit 223619f

Browse files
committed
Merge branch 'dev/0.5.3' into 'main'
Dev/0.5.3 See merge request computer-vision/kwplot!23
2 parents 553f984 + fa2722f commit 223619f

13 files changed

Lines changed: 1077 additions & 50 deletions

‎.gitlab-ci.yml‎

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,7 @@ deploy/wheels:
497497
done
498498
- |-
499499
# Have the server git-tag the release and push the tags
500-
export VERSION=$(python -c "import setup; print(setup.VERSION)")
500+
export PROJECT_VERSION=$(python -c "import setup; print(setup.VERSION)")
501501
# do sed twice to handle the case of https clone with and without a read token
502502
URL_HOST=$(git remote get-url origin | sed -e 's|https\?://.*@||g' | sed -e 's|https\?://||g' | sed -e 's|git@||g' | sed -e 's|:|/|g')
503503
source dev/secrets_configuration.sh
@@ -509,37 +509,74 @@ deploy/wheels:
509509
git config user.email "ci@gitlab.org.com"
510510
git config user.name "Gitlab-CI"
511511
fi
512-
TAG_NAME="v${VERSION}"
512+
TAG_NAME="v${PROJECT_VERSION}"
513513
echo "TAG_NAME = $TAG_NAME"
514514
if [ $(git tag -l "$TAG_NAME") ]; then
515515
echo "Tag already exists"
516516
else
517517
# if we messed up we can delete the tag
518518
# git push origin :refs/tags/$TAG_NAME
519519
# and then tag with -f
520-
git tag $TAG_NAME -m "tarball tag $VERSION"
520+
git tag $TAG_NAME -m "tarball tag $PROJECT_VERSION"
521521
git push --tags "https://git-push-token:${PUSH_TOKEN}@${URL_HOST}"
522522
fi
523523
- |-
524524
# https://docs.gitlab.com/ee/ci/variables/predefined_variables.html
525-
echo "CI_PROJECT_URL=$CI_PROJECT_URL"
526525
echo "CI_PROJECT_ID=$CI_PROJECT_ID"
527526
echo "CI_PROJECT_NAME=$CI_PROJECT_NAME"
528-
echo "CI_PROJECT_NAMESPACE=$CI_PROJECT_NAMESPACE"
529527
echo "CI_API_V4_URL=$CI_API_V4_URL"
530528
531529
export PROJECT_VERSION=$(python -c "import setup; print(setup.VERSION)")
532530
echo "PROJECT_VERSION=$PROJECT_VERSION"
533531
534532
# --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" \
533+
534+
# We will loop over all of the assets in the wheelhouse (i.e.
535+
# dist) and upload them to a package registery. We also store
536+
# the links to the artifacts so we can attach them to a release
537+
# page.
538+
PACKAGE_ARTIFACT_ARRAY=()
535539
for FPATH in "dist"/*; do
536540
FNAME=$(basename $FPATH)
537541
echo $FNAME
542+
PACKAGE_URL="$CI_API_V4_URL/projects/$CI_PROJECT_ID/packages/generic/$CI_PROJECT_NAME/$PROJECT_VERSION/$FNAME"
538543
curl \
539544
--header "JOB-TOKEN: $CI_JOB_TOKEN" \
540545
--upload-file $FPATH \
541-
"$CI_API_V4_URL/projects/$CI_PROJECT_ID/packages/generic/$CI_PROJECT_NAME/$PROJECT_VERSION/$FNAME"
546+
"$PACKAGE_URL"
547+
PACKAGE_ARTIFACT_ARRAY+=("$PACKAGE_URL")
542548
done
549+
- |-
550+
export PROJECT_VERSION=$(python -c "import setup; print(setup.VERSION)")
551+
echo "PROJECT_VERSION=$PROJECT_VERSION"
552+
TAG_NAME="v$PROJECT_VERSION"
553+
554+
# Construct the JSON for assets to attach to the release
555+
RELEASE_ASSET_JSON_LINKS=()
556+
for ASSET_URL in "${PACKAGE_ARTIFACT_ARRAY[@]}"; do
557+
ASSET_FNAME=$(basename $ASSET_URL)
558+
RELEASE_ASSET_JSON_LINKS+=("{\"name\": \"$ASSET_FNAME\", \"url\": \"$ASSET_URL\"},")
559+
done
560+
_ASSET_LINK_JSON="${RELEASE_ASSET_JSON_LINKS[@]}"
561+
# remove the trailing comma
562+
ASSET_LINK_JSON=${_ASSET_LINK_JSON::-1}
563+
echo "ASSET_LINK_JSON=$ASSET_LINK_JSON"
564+
565+
# Build json describing the release
566+
RELEASE_DATA_JSON="{
567+
\"name\": \"Version $PROJECT_VERSION\",
568+
\"description\": \"Automated release of $CI_PROJECT_NAME version $PROJECT_VERSION\",
569+
\"tag_name\": \"$TAG_NAME\",
570+
\"assets\": {\"links\": [$ASSET_LINK_JSON]}
571+
}"
572+
echo "$RELEASE_DATA_JSON"
573+
574+
curl \
575+
--header 'Content-Type: application/json' \
576+
--header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" \
577+
--data "$RELEASE_DATA_JSON" \
578+
--request POST \
579+
"$CI_API_V4_URL/projects/$CI_PROJECT_ID/releases"
543580
544581
545582
# end

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
55

66
## Version 0.5.3 - Unreleased
77

8+
### Fix
9+
* Fixed `ffmpeg_animate_frames` to handle paths and ndarrays
10+
11+
### Added
12+
* Add Palette class
13+
* Add relabel argument to FigureManager to automatically relabel on finalize
14+
815

916
## Version 0.5.2 - Released 2024-09-09
1017

‎kwplot/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ def __getattr__(name):
286286
],
287287
'util_seaborn': [
288288
'MonkeyPatchPyPlotFigureContext',
289+
'Palette',
289290
],
290291
},
291292
)

‎kwplot/cli/adjust_gui.py‎

Lines changed: 146 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ class AdjustGuiConfig(scfg.DataConfig):
3232
Helper to find good robust normalization parameters for input images.
3333
"""
3434
img_fpath = scfg.Value(None, help='input', position=1)
35+
scaling = 'sigmoid'
36+
extrema = 'quantile'
37+
low = 0.1
38+
mid = 0.5
39+
high = 0.9
40+
crop = scfg.Value("null", type=str, help='A crop string e.g. y1:y2, x1:x2')
41+
expr = scfg.Value("null", type=str)
42+
cmap = scfg.Value('None', type=str)
3543

3644

3745
def report_thread_error(fn):
@@ -179,7 +187,7 @@ def qt_is_editable(self):
179187

180188
def qt_set_value(self, data):
181189
# TODO: casting
182-
data = smartcast_mod.smartcast(data)
190+
data = smartcast_mod.smartcast(data, allow_split=False)
183191
self.value = data
184192

185193
def qt_delegate_style(self):
@@ -224,11 +232,18 @@ class QConfigNode(ub.NiceRepr, _Qt_ConfigNodeMixin):
224232
>>> 'opt1': 1,
225233
>>> 'opt2': 2,
226234
>>> },
227-
>>> 'general_opt': 'abc',
235+
>>> 'general_opt1': 'abc',
236+
>>> 'general_opt2': 'abc,efg',
228237
>>> }
229238
>>> self = QConfigNode.from_indexable(config)
230239
>>> print('self = {}'.format(ub.urepr(self, nl=1)))
231-
>>> print(self.to_indexable())
240+
>>> config = self.to_indexable()
241+
>>> print(f'config = {ub.urepr(config, nl=1)}')
242+
>>> self.children['general_opt2'].qt_set_value('fds,fds')
243+
>>> assert self.children['general_opt2'].value == 'fds,fds'
244+
>>> config = self.to_indexable()
245+
>>> print(f'config = {ub.urepr(config, nl=1)}')
246+
>>> assert config['general_opt2'] == 'fds,fds'
232247
"""
233248

234249
def __init__(self, value=NoValue, type=None, parent=None, choices=None,
@@ -538,7 +553,14 @@ class QConfigModel(QtCore.QAbstractItemModel):
538553
>>> pindex = root_config.children['algo1'].qt_get_persistant_index(self)
539554
>>> index = QtCore.QModelIndex(pindex)
540555
>>> self.setData(index, 'foo')
541-
>>> self.data(index)
556+
>>> got_value = self.data(index) # why does this not return foo?
557+
>>> config = self.root_config.to_indexable()
558+
>>> print(f'config = {ub.urepr(config, nl=1)}')
559+
>>> assert config['algo1'] == 'foo'
560+
>>> self.setData(index, 'foo,bar')
561+
>>> config = self.root_config.to_indexable()
562+
>>> print(f'config = {ub.urepr(config, nl=1)}')
563+
>>> assert config['algo1'] == 'foo,bar'
542564
"""
543565
@report_thread_error
544566
def __init__(self, root_config, parent=None):
@@ -911,6 +933,7 @@ class AdjustWidget(QtWidgets.QWidget):
911933
def __init__(self, config=None, raw_img=None):
912934
super().__init__()
913935
self.raw_img = raw_img
936+
self.processed_img = raw_img
914937
self.config = QConfigNode.coerce(config)
915938

916939
main_layout = QtWidgets.QVBoxLayout(self)
@@ -941,10 +964,38 @@ def update_normalization(self, key=None):
941964
from geowatch.cli.coco_spectra import _weighted_auto_bins
942965
import pandas as pd
943966

944-
params = self.config.to_indexable()
945967
print('Update Norm')
968+
params = self.config.to_indexable()
946969
print('params = {}'.format(ub.urepr(params, nl=1)))
947-
norm_img, norm_info = kwarray.robust_normalize(self.raw_img, params=params, return_info=True)
970+
971+
norm_param_names = {
972+
'scaling', 'extrema', 'low', 'mid', 'high'
973+
}
974+
975+
cropstr = params.get('crop', 'null')
976+
sl = parse_cropstr(cropstr)
977+
if sl is not None:
978+
self.processed_img = self.raw_img[sl]
979+
else:
980+
self.processed_img = self.raw_img
981+
982+
expr = params.get('expr', 'img')
983+
if expr == 'null' or not expr:
984+
expr = None
985+
if expr is not None:
986+
# Hack: not safe
987+
img = self.processed_img
988+
ns = globals() | locals()
989+
self.processed_img = eval(expr, ns)
990+
# self.processed_img = ns['img']
991+
992+
normalizer_params = ub.udict(params) & norm_param_names
993+
norm_img, norm_info = kwarray.robust_normalize(self.processed_img, params=normalizer_params, return_info=True)
994+
995+
processed_stats = kwarray.stats_dict(self.processed_img)
996+
norm_stats = kwarray.stats_dict(norm_img)
997+
print(f'processed_stats = {ub.urepr(processed_stats, nl=1)}')
998+
print(f'norm_stats = {ub.urepr(norm_stats, nl=1)}')
948999

9491000
self.norm_img = norm_img
9501001
self.norm_info = norm_info
@@ -954,13 +1005,23 @@ def update_normalization(self, key=None):
9541005
fig = self.mpl_widget.fig
9551006

9561007
fig.clf()
957-
ax1 = fig.add_subplot(1, 2, 1)
958-
ax2 = fig.add_subplot(1, 2, 2)
959-
960-
ax1.imshow(self.norm_img)
1008+
nSubplots = 2
1009+
SHOW_NORM_HIST = True
1010+
if SHOW_NORM_HIST:
1011+
nSubplots += 1
1012+
ax1 = fig.add_subplot(1, nSubplots, 1)
1013+
ax2 = fig.add_subplot(1, nSubplots, 2)
1014+
if SHOW_NORM_HIST:
1015+
ax3 = fig.add_subplot(1, nSubplots, 3)
1016+
1017+
imshow_params = {}
1018+
cmap = params.get('cmap', None)
1019+
if cmap is not None and cmap.lower() not in {'null', 'none'}:
1020+
imshow_params['cmap'] = cmap
1021+
ax1.imshow(self.norm_img, **imshow_params)
9611022
ax1.grid(False)
9621023

963-
counts, bins = np.histogram(self.raw_img, bins=256)
1024+
counts, bins = np.histogram(self.processed_img, bins=256)
9641025
centers = (bins[1:] + bins[0:-1]) / 2
9651026
data = pd.DataFrame({'value': centers, 'weight': counts})
9661027
n_equal_bins = _weighted_auto_bins(data, 'value', 'weight')
@@ -994,6 +1055,21 @@ def update_normalization(self, key=None):
9941055
ax2.plot([mid_val, mid_val], [ymin, ymax], '-', color='blue')
9951056
ax2.plot([max_val, max_val], [ymin, ymax], '-', color='blue')
9961057

1058+
if SHOW_NORM_HIST:
1059+
counts, bins = np.histogram(self.norm_img, bins=256)
1060+
centers = (bins[1:] + bins[0:-1]) / 2
1061+
data = pd.DataFrame({'value': centers, 'weight': counts})
1062+
n_equal_bins = _weighted_auto_bins(data, 'value', 'weight')
1063+
hist_data_kw = dict(
1064+
x='value',
1065+
weights='weight',
1066+
bins=n_equal_bins,
1067+
)
1068+
hist_style_kw = dict(
1069+
)
1070+
hist_data_kw_ = hist_data_kw.copy()
1071+
sns.histplot(ax=ax3, data=data, **hist_data_kw_, **hist_style_kw)
1072+
9971073
fig.canvas.draw()
9981074

9991075
def on_mpl_widget_click(self, event):
@@ -1007,7 +1083,7 @@ def on_mpl_widget_click(self, event):
10071083
keys = ['low', 'mid', 'high']
10081084
qarr = [config.children[k].value for k in keys]
10091085

1010-
raw_data = self.raw_img.ravel().copy()
1086+
raw_data = self.processed_img.ravel().copy()
10111087
raw_data.sort()
10121088

10131089
clicked_percentile = stats.percentileofscore(raw_data, event.xdata)
@@ -1025,6 +1101,56 @@ def on_mpl_widget_click(self, event):
10251101
model.setData(index, clicked_quantile)
10261102

10271103

1104+
def parse_cropstr(cropstr, error_policy='return-none'):
1105+
"""
1106+
Parse a string of the form R1:R2,C1:C2 into a tuple of slices using regex.
1107+
1108+
Args:
1109+
cropstr: A string specifying row and column ranges in format "R1:R2,C1:C2"
1110+
(empty values mean unbounded, e.g. "10:, :20" means from 10 to end,
1111+
and from beginning to 20)
1112+
1113+
Returns:
1114+
A tuple of slice objects (row_slice, col_slice)
1115+
1116+
Example:
1117+
>>> # xdoctest: +REQUIRES(module:PyQt5)
1118+
>>> parse_cropstr("null")
1119+
None
1120+
>>> parse_cropstr("")
1121+
(slice(None, None, None), slice(None, None, None))
1122+
>>> parse_cropstr("10:20,30:40")
1123+
(slice(10, 20), slice(30, 40))
1124+
>>> parse_cropstr(":20,30:")
1125+
(slice(None, 20), slice(30, None))
1126+
"""
1127+
import re
1128+
if not cropstr.strip():
1129+
return (slice(None), slice(None))
1130+
1131+
# Regex pattern to match either:
1132+
# 1. empty string (treated as full slice)
1133+
# 2. a single number (e.g. "5" becomes 5:6)
1134+
# 3. a range with start:end (either can be empty)
1135+
pattern = r'^\s*((?P<r1>\d*):(?P<r2>\d*))\s*,\s*((?P<c1>\d*):(?P<c2>\d*))\s*$'
1136+
1137+
match = re.fullmatch(pattern, cropstr)
1138+
if not match:
1139+
if error_policy == 'return-none':
1140+
return None
1141+
raise ValueError(f"Invalid crop string format: '{cropstr}'. Expected 'R1:R2,C1:C2'")
1142+
1143+
def to_int_or_none(s):
1144+
return int(s) if s else None
1145+
1146+
r1 = to_int_or_none(match.group('r1'))
1147+
r2 = to_int_or_none(match.group('r2'))
1148+
c1 = to_int_or_none(match.group('c1'))
1149+
c2 = to_int_or_none(match.group('c2'))
1150+
1151+
return (slice(r1, r2), slice(c1, c2))
1152+
1153+
10281154
def main(cmdline=1, **kwargs):
10291155
config = AdjustGuiConfig.cli(cmdline=cmdline, data=kwargs, strict=True)
10301156
print('config = ' + ub.urepr(dict(config), nl=1))
@@ -1046,11 +1172,14 @@ def main(cmdline=1, **kwargs):
10461172
raw_img = kwimage.grab_test_image()
10471173

10481174
config = {
1049-
'scaling': QConfigNode('sigmoid', choices=['sigmoid', 'linear']),
1050-
'extrema': QConfigNode('quantile', choices=['quantile', 'adaptive-quantile', 'iqr', 'iqr-clip']),
1051-
'low': QConfigNode(0.1, min_value=0.0, max_value=1.0, step_value=0.01),
1052-
'mid': QConfigNode(0.5, min_value=0.0, max_value=1.0, step_value=0.01),
1053-
'high': QConfigNode(0.9, min_value=0.0, max_value=1.0, step_value=0.01),
1175+
'scaling': QConfigNode(config.scaling, choices=['sigmoid', 'linear']),
1176+
'extrema': QConfigNode(config.extrema, choices=['quantile', 'adaptive-quantile', 'iqr', 'iqr-clip']),
1177+
'low': QConfigNode(config.low, min_value=0.0, max_value=1.0, step_value=0.01),
1178+
'mid': QConfigNode(config.mid, min_value=0.0, max_value=1.0, step_value=0.01),
1179+
'high': QConfigNode(config.high, min_value=0.0, max_value=1.0, step_value=0.01),
1180+
'crop': QConfigNode(config.crop),
1181+
'expr': QConfigNode(config.expr),
1182+
'cmap': QConfigNode(config.cmap),
10541183
}
10551184

10561185
widget = AdjustWidget(config, raw_img)

‎kwplot/cli/gifify.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ class Gifify(scfg.DataConfig):
2323

2424
image_list = scfg.Value(None, required=True, help=ub.paragraph(
2525
'''
26-
list of images (or a text file containing a list of images)
26+
a list of images, a text file containing a list of images, or a
27+
directory containing a list of images.
2728
'''), position=1, nargs='*', alias=['input'])
2829
delay = scfg.Value(10, type=float, short_alias=['d'], help='delay between frames', nargs=1)
2930
output = scfg.Value('auto', short_alias=['o'], help=ub.paragraph(

0 commit comments

Comments
 (0)