1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778
|
# -*- coding: utf-8; -*-
"""
Copyright (c) 2018 Rolf Hempel, rolf6419@gmx.de
This file is part of the PlanetarySystemStacker tool (PSS).
https://github.com/Rolf-Hempel/PlanetarySystemStacker
PSS is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with PSS. If not, see <http://www.gnu.org/licenses/>.
"""
from datetime import datetime
from os import unlink
from sys import stdout
from time import time, sleep
from cv2 import CV_32F, Laplacian, VideoWriter_fourcc, VideoWriter, FONT_HERSHEY_SIMPLEX, LINE_AA, \
putText, GaussianBlur, cvtColor, COLOR_BGR2HSV, COLOR_HSV2BGR, BORDER_DEFAULT, meanStdDev,\
resize, matchTemplate, minMaxLoc, TM_CCORR_NORMED, bilateralFilter, INTER_CUBIC
from numpy import abs as np_abs
from numpy import diff, average, hypot, sqrt, unravel_index, argmax, zeros, arange, array, matmul, \
empty, argmin, stack, sin, uint8, float32, uint16, full
from math import exp
from numpy import min as np_min
from numpy.fft import fft2, ifft2
from numpy.linalg import solve
from scipy.ndimage import sobel
from exceptions import DivideByZeroError, ArgumentError, Error
class Miscellaneous(object):
"""
This class provides static methods for various auxiliary purposes.
"""
@staticmethod
def quality_measure(frame):
"""
Measure the amount of structure in a rectangular frame in both coordinate directions and
return the minimum value.
:param frame: 2D image
:return: Measure for amount of local structure in the image (scalar)
"""
# Compute for each point the local gradient in both coordinate directions.
dx = diff(frame)
dy = diff(frame, axis=0)
# Compute the sharpness per coordinate direction as the 1-norm of point values.
sharpness_x = average(np_abs(dx))
sharpness_y = average(np_abs(dy))
# Return the sharpness in the direction where it is minimal.
return min(sharpness_x, sharpness_y)
@staticmethod
def quality_measure_threshold(frame, black_threshold=40.):
"""
This is an alternative method for computing the amount of local structure. Here the
summation only takes into account points where the luminosity exceeds a certain
threshold.
:param frame: 2D image
:param black_threshold: Threshold for points to be considered
:return:
"""
sum_horizontal = abs((frame[:, 2:] - frame[:, :-2])[frame[:, 1:-1] > black_threshold]).sum()
sum_vertical = abs((frame[2:, :] - frame[:-2, :])[frame[1:-1, :] > black_threshold]).sum()
return min(sum_horizontal, sum_vertical)
@staticmethod
def quality_measure_threshold_weighted(frame, stride=2, black_threshold=40., min_fraction=0.7):
"""
This is an alternative method for computing the amount of local structure. Here the
summation only takes into account points where the luminosity exceeds a certain
threshold. Additionally, if not too many points are discarded, the reduced sampling size is
compensated.
:param frame: 2D image
:param stride: Stride for gradient computation. For blurry images increase this value.
:param black_threshold: Threshold for points to be considered.
:param min_fraction: Minimum fraction of points to pass the threshold.
:return:
"""
frame_size = frame.shape[0] * frame.shape[1]
stride_2 = 2 * stride
# Compute a mask for all pixels which are bright enough (to avoid background noise).
mask = frame > black_threshold
mask_fraction = mask.sum() / frame_size
# If most pixels are bright enough, compensate for different pixel counts.
if mask_fraction > min_fraction:
sum_horizontal = abs((frame[:, stride_2:] - frame[:, :-stride_2])[mask[:, stride:-stride]]).sum() / mask_fraction
sum_vertical = abs((frame[stride_2:, :] - frame[:-stride_2, :])[mask[stride:-stride, :]]).sum() / mask_fraction
# If many pixels are too dim, penalize this patch by not compensating for pixel count.
else:
sum_horizontal = abs((frame[:, stride_2:] - frame[:, :-stride_2])[mask[:, stride:-stride]]).sum()
sum_vertical = abs((frame[stride_2:, :] - frame[:-stride_2, :])[mask[stride:-stride, :]]).sum()
return min(sum_horizontal, sum_vertical)
@staticmethod
def local_contrast_laplace(frame, stride):
"""
Measure the amount of structure in a rectangular frame in both coordinate directions and
return the minimum value. The discrete variance of the Laplacian is computed.
:param frame: 2D image
:param stride: Factor for down-sampling
:return: Measure for amount of local structure in the image (scalar)
"""
# sharpness = sum(laplace(frame[::stride, ::stride])**2)
return meanStdDev(Laplacian(frame[::stride, ::stride], CV_32F))[1][0][0]
@staticmethod
def local_contrast_sobel(frame, stride):
"""
Compute a measure for local contrast in an image using the Sobel method.
:param frame: 2D image
:param stride: Factor for down-sampling
:return: Overall sharpness measure (scalar)
"""
frame_int32 = frame[::stride, ::stride].astype('int32')
dx = sobel(frame_int32, 0) # vertical derivative
dy = sobel(frame_int32, 1) # horizontal derivative
mag = hypot(dx, dy) # magnitude
return mag.sum(axis=0)
@staticmethod
def local_contrast(frame, stride):
"""
Compute a measure for local contrast in an image based on local gradients. Down-sample the
image by factor "stride" before measuring the contrast.
:param frame: 2D image
:param stride: Factor for down-sampling
:return: Overall measure for local contrast (scalar)
"""
frame_strided = frame[::stride, ::stride]
# Remove a row or column, respectively, to make the dx and dy arrays of the same shape.
dx = diff(frame_strided)[1:, :] # remove the first row
dy = diff(frame_strided, axis=0)[:, 1:] # remove the first column
dnorm = hypot(dx, dy)
return average(dnorm)
@staticmethod
def translation(frame_0, frame_1, shape):
"""
Compute the translation vector of frame_1 relative to frame_0 using phase correlation.
:param frame_0: Reference frame
:param frame_1: Frame shifted slightly relative to frame_0
:param shape: Shape of frames
:return: [shift in y, shift in x] of frame_1 relative to frame_0. More precisely:
frame_1 must be shifted by this amount to register with frame_0.
"""
# Compute the fast Fourier transforms of both frames.
f0 = fft2(frame_0)
f1 = fft2(frame_1)
# Compute the phase correlation. The resulting image has a bright peak at the pixel location
# which corresponds to the shift vector.
ir = abs(ifft2((f0 * f1.conjugate()) / (abs(f0) * abs(f1))))
# Compute the pixel coordinates of the image maximum.
ty, tx = unravel_index(argmax(ir), shape)
# Bring the shift values as close as possible to the coordinate origin.
if ty > shape[0] // 2:
ty -= shape[0]
if tx > shape[1] // 2:
tx -= shape[1]
return [ty, tx]
@staticmethod
def multilevel_correlation(reference_box_first_phase, frame_mono_blurred,
blurr_strength_first_phase, reference_box_second_phase,
y_low, y_high, x_low, x_high, search_width,
weight_matrix_first_phase=None, subpixel_solve=False):
"""
Determine the local warp shift at an alignment point using a multi-level approach based on
normalized cross correlation. The first level uses a pixel grid which is coarser by a factor
of two in both coordinate directions. The second level uses the original pixel grid.
An additional noise reduction is applied on the first level as chosen by the corresponding
blurring parameter.
The global search width is split between the two phases: A fixed search width of 4 is used
in the second phase (only for local corrections). Therefore, a width of (search_width-4)
in each coordinate direction remains for the first phase.
In both phases it is determined if the optimum is attained on the border of the search
area. If so, the correlation is regarded as unsuccessful (because the real optimum may be
outside of the search area).
:param reference_box_first_phase: Image box with stride 2 around alignment point in the
locally sharpest frame. A Gaussian filter with strength
"blurr_strength_first_phase" has been applied.
:param frame_mono_blurred: Given frame (stride 1) for which the local shift at the alignment
point is to be computed. This is the Gaussian blurred version of
the monochrome frame as computed in class "frames".
:param blurr_strength_first_phase: Additional Gaussian blur strength to be applied to
images in the first phase.
:param reference_box_second_phase: Image box with stride 1 around alignment point in the
locally sharpest frame.
:param y_low: Lower y coordinate limit of box in given frame, taking into account the
global shift and the different sizes of the mean frame and the original
frames.
:param y_high: Upper y coordinate limit.
:param x_low: Lower x coordinate limit.
:param x_high: Upper x coordinate limit.
:param search_width: Maximum distance in y and x from origin of the search area. (See the
comment in the explanatory text above.)
:param weight_matrix_first_phase: This parameter (if not None) may give a weighting array
by which the cross correlation results are multiplied
before the maximum value is determined. The size of this
2D array in each coordinate direction is that of the
reference_box_first_phase plus two times the first phase
search width (see below).
:param subpixel_solve: If True, in the second phase the optimum is computed with
sub-pixel accuracy (i.e. the returned shifts are not integer).
If False, shifts are computed as integer values.
:return: (shift_y_local_first_phase, shift_x_local_first_phase, success_first_phase,
shift_y_local_second_phase, shift_x_local_second_phase, success_second_phase)
with:
shift_y_local_first_phase: Local y warp shift determined in first phase
(expressed in terms of the original pixel grid).
shift_x_local_first_phase: Local x warp shift determined in first phase.
success_first_phase: "True" if the optimum was attained in the interior of the
search domain. "False" otherwise.
shift_y_local_second_phase: Local y warp shift determined in second phase.
shift_x_local_second_phase: Local x warp shift determined in second phase.
success_second_phase: "True" if the optimum was attained in the interior of the
search domain. "False" otherwise.
"""
# The optimization in the second phase is only meant for local fine grid adjustments.
search_width_second_phase = 4
# In the first phase the largest part of the local warp is detected. Divide the search
# width by two because the first phase uses a coarser pixel grid.
search_width_first_phase = int((search_width - search_width_second_phase) / 2)
# Define a window around the alignment point box. Extend the window in each coordinate
# direction. This defines the search space for the template matching. Coarsen the grid
# by a factor of two and apply an additional Gaussian blur.
index_extension = search_width_first_phase * 2
frame_window_first_phase = GaussianBlur(frame_mono_blurred[
y_low - index_extension:y_high + index_extension:2,
x_low - index_extension:x_high + index_extension:2],
(blurr_strength_first_phase,
blurr_strength_first_phase), 0)
# Compute the normalized cross correlation.
result = matchTemplate((frame_window_first_phase).astype(float32),
reference_box_first_phase.astype(float32), TM_CCORR_NORMED)
# Determine the position of the maximum correlation and compute the corresponding warp
# shift. The factor of 2 transforms the shift to the fine pixel grid. If a non-trivial
# weight matrix is specified, multiply the results before looking for the maximum position.
if weight_matrix_first_phase is not None:
minVal, maxVal, minLoc, maxLoc = minMaxLoc(result * weight_matrix_first_phase)
else:
minVal, maxVal, minLoc, maxLoc = minMaxLoc(result)
shift_y_local_first_phase = (search_width_first_phase - maxLoc[1]) * 2
shift_x_local_first_phase = (search_width_first_phase - maxLoc[0]) * 2
# The first phase is regarded as successful if the maximum correlation was attained in the
# interior of the search space.
success_first_phase = abs(shift_y_local_first_phase) != index_extension and abs(
shift_x_local_first_phase) != index_extension
# If the first phase was successful, add a second phase with a local search on the finest
# (original) pixel grid.
if success_first_phase:
# Define the search window for the second phase. Apply the shift found in the
# first phase.
y_lo = y_low - shift_y_local_first_phase - search_width_second_phase
y_hi = y_high - shift_y_local_first_phase + search_width_second_phase
x_lo = x_low - shift_x_local_first_phase - search_width_second_phase
x_hi = x_high - shift_x_local_first_phase + search_width_second_phase
# Cut out the frame window for the second phase (fine grid) correlation.
frame_window_second_phase = frame_mono_blurred[y_lo:y_hi, x_lo:x_hi]
# Perform the template matching on the fine grid, again using normalized cross
# correlation.
result = matchTemplate(frame_window_second_phase.astype(float32),
reference_box_second_phase, TM_CCORR_NORMED)
# Find the position of the local optimum and compute the corresponding shift values.
minVal, maxVal, minLoc, maxLoc = minMaxLoc(result)
shift_y_local_second_phase = search_width_second_phase - maxLoc[1]
shift_x_local_second_phase = search_width_second_phase - maxLoc[0]
# Again, the second phase is deemed successful only if the optimum was attained in the
# interior of the search space.
success_second_phase = abs(
shift_y_local_second_phase) != search_width_second_phase and abs(
shift_x_local_second_phase) != search_width_second_phase
# If the second phase was not successful, set the corresponding shifts to zero.
if not success_second_phase:
shift_y_local_second_phase = shift_x_local_second_phase = 0
# The following code computes the sub-pixel shift correction to be used in drizzling.
elif subpixel_solve:
# Cut a 3x3 window around the optimum from the matching results.
surroundings = result[maxLoc[1]-1:maxLoc[1]+2, maxLoc[0]-1:maxLoc[0]+2]
try:
# Compute the correction to the center position. Only if the found sub-pixel
# correction is within the 3x3 box, it is trusted (and used).
y_corr, x_corr = Miscellaneous.sub_pixel_solve(surroundings)
if abs(y_corr) <= 1. and abs(x_corr) <= 1.:
shift_y_local_second_phase -= y_corr
shift_x_local_second_phase -= x_corr
except:
# print ("Subpixel solve not successful")
pass
# If the first phase was unsuccessful, drop the second phase and set all warp shifts to 0.
else:
success_second_phase = False
shift_y_local_first_phase = shift_x_local_first_phase = shift_y_local_second_phase = \
shift_x_local_second_phase = 0
return shift_y_local_first_phase, shift_x_local_first_phase, success_first_phase, \
shift_y_local_second_phase, shift_x_local_second_phase, success_second_phase
@staticmethod
def search_local_match(reference_box, frame, y_low, y_high, x_low, x_high, search_width,
sampling_stride, sub_pixel=True):
"""
Try shifts in y, x between the box around the alignment point in the mean frame and the
corresponding box in the given frame. Start with shifts [0, 0] and move out in a circular
fashion, until the radius "search_width" is reached. The global frame shift is accounted for
beforehand already.
:param reference_box: Image box around alignment point in mean frame.
:param frame: Given frame for which the local shift at the alignment point is to be
computed.
:param y_low: Lower y coordinate limit of box in given frame, taking into account the
global shift and the different sizes of the mean frame and the original
frames.
:param y_high: Upper y coordinate limit
:param x_low: Lower x coordinate limit
:param x_high: Upper x coordinate limit
:param search_width: Maximum radius of the search spiral
:param sampling_stride: Stride in both coordinate directions used in computing deviations
:param sub_pixel: If True, compute local shifts with sub-pixel accuracy
:return: ([shift_y, shift_x], [min_r]) with:
shift_y, shift_x: shift values of minimum or [0, 0] if no optimum could be found.
[dev_r]: list of minimum deviations for radius r=0 ... r_max, where r_max is the
widest search radius tested.
"""
# Initialize the global optimum with an impossibly large value.
deviation_min = 1.e30
dy_min = None
dx_min = None
# Initialize list of minimum deviations for each search radius and field of deviations.
dev_r = []
deviations = zeros((2 * search_width + 1, 2 * search_width + 1))
# Start with shift [0, 0] and proceed in a circular pattern.
for r in arange(search_width + 1):
# Create an enumerator which produces shift values [dy, dx] in a circular pattern
# with radius "r".
circle_r = Miscellaneous.circle_around(0, 0, r)
# Initialize the optimum for radius "r" to an impossibly large value,
# and the corresponding shifts to None.
deviation_min_r, dy_min_r, dx_min_r = 1.e30, None, None
# Go through the circle with radius "r" and compute the difference (deviation)
# between the shifted frame and the corresponding box in the mean frame. Find the
# minimum "deviation_min_r" for radius "r".
if sampling_stride != 1:
for (dy, dx) in circle_r:
deviation = abs(
reference_box[::sampling_stride, ::sampling_stride] - frame[
y_low - dy:y_high - dy:sampling_stride,
x_low - dx:x_high - dx:sampling_stride]).sum()
# deviation = sqrt(square(
# reference_box - frame[y_low - dy:y_high - dy, x_low - dx:x_high - dx]).sum())
if deviation < deviation_min_r:
deviation_min_r, dy_min_r, dx_min_r = deviation, dy, dx
deviations[dy + search_width, dx + search_width] = deviation
else:
for (dy, dx) in circle_r:
deviation = abs(
reference_box - frame[y_low - dy:y_high - dy, x_low - dx:x_high - dx]).sum()
if deviation < deviation_min_r:
deviation_min_r, dy_min_r, dx_min_r = deviation, dy, dx
deviations[dy + search_width, dx + search_width] = deviation
# Append the minimal deviation for radius r to list of minima.
dev_r.append(deviation_min_r)
# If for the current radius there is no improvement compared to the previous radius,
# the optimum is reached.
if deviation_min_r >= deviation_min:
# For sub-pixel accuracy, find local minimum of fitting paraboloid.
if sub_pixel:
try:
y_correction, x_correction = Miscellaneous.sub_pixel_solve(deviations[
dy_min + search_width - 1: dy_min + search_width + 2,
dx_min + search_width - 1: dx_min + search_width + 2])
except DivideByZeroError as ex:
print(ex.message)
x_correction = y_correction = 0.
# Add the sub-pixel correction to the local shift. If the correction is above
# one pixel, something went wrong.
if abs(y_correction) < 1. and abs(x_correction) < 1.:
dy_min += y_correction
dx_min += x_correction
# else:
# print ("y_correction: " + str(y_correction) + ", x_correction: " +
# str(x_correction))
return [dy_min, dx_min], dev_r
# Otherwise, update the current optimum and continue.
else:
deviation_min, dy_min, dx_min = deviation_min_r, dy_min_r, dx_min_r
# If within the maximum search radius no optimum could be found, return [0, 0].
return [0, 0], dev_r
# Define the matrix used in method "sub_pixel_solve" to solve the normal equations. It is
# computed once and for all as "inv(a_transpose * a) * a_transpose". Using this matrix, the
# solution of the normal equations is reduced to a simple matrix multiplication.
sub_pixel_solve_matrix = [
[0.16666667, -0.33333333, 0.16666667, 0.16666667, -0.33333333, 0.16666667, 0.16666667,
-0.33333333, 0.16666667],
[0.16666667, 0.16666667, 0.16666667, -0.33333333, -0.33333333, -0.33333333, 0.16666667,
0.16666667, 0.16666667], [0.25, 0., -0.25, 0., 0., 0., -0.25, 0., 0.25],
[-0.16666667, 0., 0.16666667, -0.16666667, 0., 0.16666667, -0.16666667, 0., 0.16666667],
[-0.16666667, -0.16666667, -0.16666667, 0., 0., 0., 0.16666667, 0.16666667, 0.16666667],
[-0.11111111, 0.22222222, -0.11111111, 0.22222222, 0.55555556, 0.22222222, -0.11111111,
0.22222222, -0.11111111]]
@staticmethod
def sub_pixel_solve(function_values):
"""
Compute the sub-pixel correction for method "search_local_match".
:param function_values: Matching differences at (3 x 3) pixels around the minimum / maximum
found
:return: Corrections in y and x to the center position for local minimum / maximum
"""
# If the functions are not yet reduced to 1D, do it now.
function_values_1d = function_values.reshape((9,))
# Solve for parameters of the fitting function:
# f = a_f * x ** 2 + b_f * y ** 2 + c_f * x * y + d_f * x + e_f * y + g_f
# using normal equations. The problem is reduced to a matrix multiplication with the fixed
# system matrix defined above.
a_f, b_f, c_f, d_f, e_f, g_f = matmul(Miscellaneous.sub_pixel_solve_matrix, function_values_1d)
# print("\nSolve, coeffs: " + str((a_f, b_f, c_f, d_f, e_f, g_f)))
# The corrected pixel values of the minimum result from setting the first derivatives of
# the fitting funtion in y and x direction to zero, and solving for y and x.
denominator_y = c_f ** 2 - 4. * a_f * b_f
if abs(denominator_y) > 1.e-10 and abs(a_f) > 1.e-10:
y_correction = (2. * a_f * e_f - c_f * d_f) / denominator_y
x_correction = (- c_f * y_correction - d_f) / (2. * a_f)
elif abs(denominator_y) > 1.e-10 and abs(c_f) > 1.e-10:
y_correction = (2. * a_f * e_f - c_f * d_f) / denominator_y
x_correction = (-2. * b_f * y_correction - e_f) / c_f
else:
raise DivideByZeroError("Sub-pixel shift cannot be computed, set to zero")
return y_correction, x_correction
@staticmethod
def sub_pixel_solve_old(function_values):
"""
Compute the sub-pixel correction for method "search_local_match".
:param function_values: Matching differences at (3 x 3) pixels around the minimum found
:return: Corrections in y and x to the center position for local minimum
"""
# If the functions are not yet reduced to 1D, do it now.
function_values_1d = function_values.reshape((9,))
# There are nine equations for six unknowns. Use normal equations to solve for optimum.
a_transpose = array(
[[1., 0., 1., 1., 0., 1., 1., 0., 1.], [1., 1., 1., 0., 0., 0., 1., 1., 1.],
[1., -0., -1., -0., 0., 0., -1., 0., 1.], [-1., 0., 1., -1., 0., 1., -1., 0., 1.],
[-1., -1., -1., 0., 0., 0., 1., 1., 1.], [1., 1., 1., 1., 1., 1., 1., 1., 1.]])
a_transpose_a = array(
[[6., 4., 0., 0., 0., 6.], [4., 6., 0., 0., 0., 6.], [0., 0., 4., 0., 0., 0.],
[0., 0., 0., 6., 0., 0.], [0., 0., 0., 0., 6., 0.], [6., 6., 0., 0., 0., 9.]])
# Right hand side is "a transposed times input vector".
rhs = matmul(a_transpose, function_values_1d)
# Solve for parameters of the fitting function
# f = a_f * x ** 2 + b_f * y ** 2 + c_f * x * y + d_f * x + e_f * y + g_f
a_f, b_f, c_f, d_f, e_f, g_f = solve(a_transpose_a, rhs)
# The corrected pixel values of the minimum result from setting the first derivatives of
# the fitting funtion in y and x direction to zero, and solving for y and x.
denominator_y = c_f ** 2 - 4. * a_f * b_f
if abs(denominator_y) > 1.e-10 and abs(a_f) > 1.e-10:
y_correction = (2. * a_f * e_f - c_f * d_f) / denominator_y
x_correction = (- c_f * y_correction - d_f) / (2. * a_f)
elif abs(denominator_y) > 1.e-10 and abs(c_f) > 1.e-10:
y_correction = (2. * a_f * e_f - c_f * d_f) / denominator_y
x_correction = (-2. * b_f * y_correction - e_f) / c_f
else:
raise DivideByZeroError("Sub-pixel shift cannot be computed, set to zero")
return y_correction, x_correction
@staticmethod
def search_local_match_init(reference_frame, y_low, y_high, x_low, x_high, search_width):
"""
As an alternative to the method "search_local_match" above there is a split version
consisting of this initialization method and the execution method. For a given alignment
point the initialization has to be called once only. The data structures allocated during
initialization are re-used with each frame for which the shift against the reference frame
is to be computed. The individual frame shift is computed with the execution method below.
The split version can be better suited for execution on a graphic accelerator. The biggest
part of the data does not change between frame executions, so it has to be copied to the
graphics memory only once. The executions for individual frames only need little additional
data, and they can be performed in parallel.
The split version is not available with subpixel accuracy.
:param reference_frame: Reference frame, against which the current frame shift is to be
computed (note: The full frame, not the alignment box)
:param y_low: Lower y coordinate limit of alignment box in reference frame
:param y_high: Upper y coordinate limit of alignment box in reference frame
:param x_low: Lower x coordinate limit of alignment box in reference frame
:param x_high: Upper x coordinate limit of alignment box in reference frame
:param search_width: Maximum radius of the search spiral
:return: (reference_stack, displacements, radius_start) witn
reference_stack: Stack of shifted reference frame alignment boxes
displacements: List of [dy, dx] displacements for all stack elements
radius_start: Start indices into "reference_stack" and "displacements" for each
radius (from 0 to search_width)
"""
# Compute the maximal number of shifts to be tested.
search_dim = (2*search_width+1)**2
# Allocate permanent data structures.
window_height = y_high - y_low
window_width = x_high - x_low
reference_stack = empty([search_dim, window_height, window_width],
dtype=reference_frame.dtype)
displacements = []
radius_start = [0]
# Create the reference_stack containing shifted windows into the reference frame.
index = 0
for r in range(search_width + 1):
# Create an enumerator which produces shift values [dy, dx] in a circular pattern
# with radius "r".
for (dy, dx) in Miscellaneous.circle_around(0, 0, r):
reference_stack[index, :, :] = reference_frame[y_low + dy:y_high + dy,
x_low + dx:x_high + dx]
displacements.append([dy, dx])
index += 1
radius_start.append(index)
return (reference_stack, displacements, radius_start)
@staticmethod
def search_local_match_execute(frame_window, reference_stack, displacements, radius_start):
"""
The "execute" method of the split version of "search_local_match". This method is executed
for each frame, potentially in parallel for all frames.
:param frame_window: Window into the current frame of the same shape as specified for the
reference window in the init method. Be careful: the current frame and
the reference frame have different shapes.
:param reference_stack: Permanent object allocated by init routing (see above)
:param displacements: Permanent object allocated by init routing (see above)
:param radius_start: Permanent object allocated by init routing (see above)
:return: ([shift_y, shift_x], [min_r]) with:
shift_y, shift_x: shift values of minimum or [0, 0] if no optimum could be found.
[dev_r]: list of minimum deviations for radius r=0 ... r_max, where r_max is the
widest search radius tested.
"""
search_width_plus_1 = len(radius_start)
# Initialize the global optimum with an impossibly large value.
deviation_min = 1.e30
index_min = None
# Initialize list of minimum deviations for each search radius and field of deviations.
dev_r = []
# Compare frame_window with a stack of shifted reference frame windows stored for radius r.
for r in arange(search_width_plus_1):
temp_vec = abs(
reference_stack[radius_start[r]:radius_start[r + 1], :, :] - frame_window).sum(
axis=(1, 2))
deviation_min_r = np_min(temp_vec)
index_min_r = argmin(temp_vec) + radius_start[r]
# The same in loop notation:
#
# deviation_min_r, index_min_r = 1.e30, None
# for index in arange(radius_start[r], radius_start[r+1]):
# deviation = abs(reference_stack[index, :,:] - frame_window).sum()
# if deviation < deviation_min_r:
# deviation_min_r = deviation
# index_min_r = index
# Append the minimal deviation for radius r to list of minima.
dev_r.append(deviation_min_r)
if deviation_min_r >= deviation_min:
return displacements[index_min], dev_r
# Otherwise, update the current optimum and continue.
else:
deviation_min = deviation_min_r
index_min = index_min_r
# If within the maximum search radius no optimum could be found, return [0, 0].
return [0, 0], dev_r
@staticmethod
def search_local_match_gradient(reference_box, frame, y_low, y_high, x_low, x_high, search_width,
sampling_stride, dev_table):
"""
Try shifts in y, x between the box around the alignment point in the mean frame and the
corresponding box in the given frame. Start with shifts [0, 0] and move out in steps until
a local optimum is reached. In each step try all positions with distance 1 in y and/or x
from the optimum found in the previous step (steepest descent). The global frame shift is
accounted for beforehand already.
:param reference_box: Image box around alignment point in mean frame.
:param frame: Given frame for which the local shift at the alignment point is to be
computed.
:param y_low: Lower y coordinate limit of box in given frame, taking into account the
global shift and the different sizes of the mean frame and the original
frames.
:param y_high: Upper y coordinate limit
:param x_low: Lower x coordinate limit
:param x_high: Upper x coordinate limit
:param search_width: Maximum distance in y and x from origin of the search area
:param sampling_stride: Stride in both coordinate directions used in computing deviations
:param dev_table: Scratch table to be used internally for storing intermediate results,
size: [2*search_width, 2*search_width], dtype=float32.
:return: ([shift_y, shift_x], [min_r]) with:
shift_y, shift_x: shift values of minimum or [0, 0] if no optimum could be found.
[dev_r]: list of minimum deviations for all steps until a local minimum is found.
"""
# Set up a table which keeps deviation values from earlier iteration steps. This way,
# deviation evaluations can be avoided at coordinates which have been visited before.
# Initialize deviations with an impossibly high value.
dev_table[:, :] = 1.e30
# Initialize the global optimum with the value at dy=dx=0.
if sampling_stride != 1:
deviation_min = abs(reference_box[::sampling_stride, ::sampling_stride] - frame[
y_low:y_high:sampling_stride,
x_low:x_high:sampling_stride]).sum()
else:
deviation_min = abs(reference_box - frame[y_low:y_high, x_low:x_high]).sum()
dev_table[0, 0] = deviation_min
dy_min = 0
dx_min = 0
counter_new = 0
counter_reused = 0
# Initialize list of minimum deviations for each search radius.
dev_r = [deviation_min]
# Start with shift [0, 0]. Stop when a circle with radius 1 around the current optimum
# reaches beyond the search area.
while max(abs(dy_min), abs(dx_min)) < search_width-1:
# Create an enumerator which produces shift values [dy, dx] in a circular pattern
# with radius 1 around the current optimum [dy_min, dx_min].
circle_1 = Miscellaneous.circle_around(dy_min, dx_min, 1)
# Initialize the optimum for the new circle to an impossibly large value,
# and the corresponding shifts to None.
deviation_min_1, dy_min_1, dx_min_1 = 1.e30, None, None
# Go through the circle with radius 1 and compute the difference (deviation)
# between the shifted frame and the corresponding box in the mean frame. Find the
# minimum "deviation_min_1".
if sampling_stride != 1:
for (dy, dx) in circle_1:
deviation = dev_table[dy, dx]
if deviation > 1.e29:
counter_new += 1
deviation = abs(reference_box[::sampling_stride, ::sampling_stride] - frame[
y_low - dy:y_high - dy:sampling_stride,
x_low - dx:x_high - dx:sampling_stride]).sum()
dev_table[dy, dx] = deviation
else:
counter_reused += 1
if deviation < deviation_min_1:
deviation_min_1, dy_min_1, dx_min_1 = deviation, dy, dx
# deviation = abs(reference_box[::sampling_stride, ::sampling_stride] - frame[
# y_low - dy:y_high - dy:sampling_stride,
# x_low - dx:x_high - dx:sampling_stride]).sum()
# if deviation < deviation_min_1:
# deviation_min_1, dy_min_1, dx_min_1 = deviation, dy, dx
else:
for (dy, dx) in circle_1:
deviation = dev_table[dy, dx]
if deviation > 1.e29:
deviation = abs(
reference_box - frame[y_low - dy:y_high - dy,
x_low - dx:x_high - dx]).sum()
dev_table[dy, dx] = deviation
if deviation < deviation_min_1:
deviation_min_1, dy_min_1, dx_min_1 = deviation, dy, dx
# Append the minimal deviation found in this step to list of minima.
dev_r.append(deviation_min_1)
# If for the current center the match is better than for all neighboring points, a
# local optimum is found.
if deviation_min_1 >= deviation_min:
# print ("new: " + str(counter_new) + ", reused: " + str(counter_reused))
return [dy_min, dx_min], dev_r
# Otherwise, update the current optimum and continue.
else:
deviation_min, dy_min, dx_min = deviation_min_1, dy_min_1, dx_min_1
# If within the maximum search radius no optimum could be found, return [0, 0].
return [0, 0], dev_r
@staticmethod
def search_local_match_full(reference_box, frame, y_low, y_high, x_low, x_high,
search_width,
sampling_stride, dev_table):
"""
For all shifts with -search_width < shift < search_width in y, x between the box around the
alignment point in the mean frame and the corresponding box in the given frame compute the
deviation. Return the y and x offsets where the deviation is smallest. The global frame
shift is accounted for beforehand already.
:param reference_box: Image box around alignment point in mean frame.
:param frame: Given frame for which the local shift at the alignment point is to be
computed.
:param y_low: Lower y coordinate limit of box in given frame, taking into account the
global shift and the different sizes of the mean frame and the original
frames.
:param y_high: Upper y coordinate limit
:param x_low: Lower x coordinate limit
:param x_high: Upper x coordinate limit
:param search_width: Maximum distance in y and x from origin of the search area
:param sampling_stride: Stride in both coordinate directions used in computing deviations
:param dev_table: Scratch table to be used internally for storing intermediate results,
size: [2*search_width+1, 2*search_width+1], dtype=float32.
:return: ([shift_y, shift_x], [min_r]) with:
shift_y, shift_x: shift values of minimum or [0, 0] if no optimum could be found.
[dev_r]: list of minimum deviations for all steps until a local minimum is found.
"""
# Set up a table which keeps deviation values from earlier iteration steps. This way,
# deviation evaluations can be avoided at coordinates which have been visited before.
# Initialize deviations with an impossibly high value.
dev_table[:, :] = 1.e30
if sampling_stride != 1:
for dy in range(-search_width, search_width + 1):
for dx in range(-search_width, search_width + 1):
dev_table[search_width + dy, search_width + dx] = abs(
reference_box[::sampling_stride, ::sampling_stride] - frame[
y_low - dy:y_high - dy:sampling_stride,
x_low - dx:x_high - dx:sampling_stride]).sum()
else:
for dy in range(-search_width, search_width + 1):
for dx in range(-search_width, search_width + 1):
dev_table[search_width + dy, search_width + dx] = abs(
reference_box - frame[y_low - dy:y_high - dy, x_low - dx:x_high - dx]).sum()
dy, dx = unravel_index(argmin(dev_table, axis=None), dev_table.shape)
# Return the coordinate shifts for the minimum position and the value at that position.
return [dy-search_width, dx-search_width], dev_table[dy, dx]
@staticmethod
def auto_rgb_align(input_image, max_shift, interpolation_factor=1, reduce_output=True,
blur_strength=None):
"""
Align the three color channels of an RGB image automatically. For sub-pixel resolution the
image can be interpolated before the shift is measured. Optionally, a Gaussian blur can be
added to suppress noise.
If no matching shift can be found within the range given by "max_shift", an Exception of
type Error is thrown.
:param input_image: Three-channel RGB image.
:param max_shift: Maximal displacement between channels.
:param interpolation_factor: Scaling factor (integer) for subpixel measurements.
:param reduce_output: If True, the corrected image is reduced to the input resolution.
If False, it stays at the interpolated resolution. In this case the
output values for correction_red and correction_blue are in terms of
the interpolated resolution as well.
:param blur_strength: Optional blur strength, must be an uneven integer > 0.
:return: (corrected_image, correction_red, correction_blue) with:
corrected_image: The corrected image with the same datatype as the input image.
Please note that the size may be reduced because of channel
mismatch at the borders.
correction_red: Tuple (shift_y, shift_x) with coordinate shifts in y and x
applied to the red channel of input_image to produce the
corrected_image.
correction_blue: Tuple (shift_y, shift_x) with coordinate shifts in y and x
applied to the blue channel of input_image to produce the
corrected_image.
"""
# sleep(2.)
# Immediately return for monochrome input.
if len(input_image.shape) != 3:
return input_image
# If subpixel resolution is asked for, interpolate the input image first.
if interpolation_factor != 1:
input_interpolated = Miscellaneous.shift_colors(input_image, (0, 0), (0, 0),
interpolate_input=interpolation_factor)
else:
input_interpolated = input_image
# Measure the shifts of the red and blue channels, respectively, with respect to the green
# channel.
channel_green = 1
channel_red = 0
shift_red = Miscellaneous.measure_rgb_shift(input_interpolated, channel_red,
channel_green, max_shift * interpolation_factor,
blur_strength=blur_strength)
channel_blue = 2
shift_blue = Miscellaneous.measure_rgb_shift(input_interpolated, channel_blue,
channel_green, max_shift*interpolation_factor,
blur_strength=blur_strength)
# Reverse the shift measured in the input image.
if reduce_output:
factor = interpolation_factor
else:
factor = 1
return Miscellaneous.shift_colors(input_interpolated, (-shift_red[0], -shift_red[1]),
(-shift_blue[0], -shift_blue[1]), reduce_output=factor), \
(-shift_red[0]/factor, -shift_red[1]/factor), \
(-shift_blue[0]/factor, -shift_blue[1]/factor)
@staticmethod
def shift_colors(input_image, shift_red, shift_blue, interpolate_input=1, reduce_output=1):
"""
Shift the red and blue channel of a color image in y and x direction, and leave the green
channel unchanged. The shift is sub-pixel accurate if the input is interpolated. Optionally
the resulting image is reduced in size by a given factor.
:param input_image: Three-channel RGB image.
:param shift_red: Tuple (shift_y, shift_x) with shifts in y and x direction for the red
channel. After multiplication with the interpolate_input factor (if given)
the shifts are rounded to the next integer value. This enables sub-pixel
shifts at the original image scale.
:param shift_blue: Tuple (shift_y, shift_x) with shifts in y and x direction for the blue
channel.
:param interpolate_input: If set, it must be an integer >= 1. The input image is
interpolated in both directions by this factor before the shift is
applied.
:param reduce_output: If set, it must be an integer >= 1. The intermediate image is reduced
in size by this factor. Please note that interpolate_input =
reduce_output assures that the input and output images are the same
size.
:return: Three-channel RGB image with the color shifts applied.
"""
# sleep(1.)
# Immediately return for monochrome input.
if len(input_image.shape) != 3:
return input_image
# If all shifts are zero, nothing is to be done except interpolation / reduction.
if not (shift_red[0] or shift_red[1] or shift_blue[0] or shift_blue[1]):
# If the factors for interpolation and reduction are the same, nothing is to be done.
if not (interpolate_input - reduce_output) or not reduce_output:
return input_image
# Simple resizing. reduce_output is not zero (see above)!
else:
scale_factor = float(interpolate_input) / float(reduce_output)
return resize(input_image, (round(input_image.shape[1] * scale_factor),
round(input_image.shape[0] * scale_factor)),
interpolation=INTER_CUBIC)
# If interpolation is requested, resize the input image and multiply the shift values.
if interpolate_input != 1:
dim_y = input_image.shape[0] * interpolate_input
dim_x = input_image.shape[1] * interpolate_input
interp_image = resize(input_image, (dim_x, dim_y), interpolation=INTER_CUBIC)
s_red_y = round(interpolate_input * shift_red[0])
s_red_x = round(interpolate_input * shift_red[1])
s_blue_y = round(interpolate_input * shift_blue[0])
s_blue_x = round(interpolate_input * shift_blue[1])
else:
dim_y = input_image.shape[0]
dim_x = input_image.shape[1]
interp_image = input_image
s_red_y = round(shift_red[0])
s_red_x = round(shift_red[1])
s_blue_y = round(shift_blue[0])
s_blue_x = round(shift_blue[1])
# Remember the data type of the input image. The output image will be of the same type.
type = input_image.dtype
# In the following, for both the red and blue channels the index areas are computed for
# which no shift is beyond the image dimensions. First treat the y coordinate.
y_low_source_r = -s_red_y
y_high_source_r = dim_y - s_red_y
y_low_target_r = 0
# If the shift reaches beyond the frame, reduce the copy area.
if y_low_source_r < 0:
y_low_target_r = -y_low_source_r
y_low_source_r = 0
if y_high_source_r > dim_y:
y_high_source_r = dim_y
y_high_target_r = y_low_target_r + y_high_source_r - y_low_source_r
y_low_source_b = -s_blue_y
y_high_source_b = dim_y - s_blue_y
y_low_target_b = 0
# If the shift reaches beyond the frame, reduce the copy area.
if y_low_source_b < 0:
y_low_target_b = -y_low_source_b
y_low_source_b = 0
if y_high_source_b > dim_y:
y_high_source_b = dim_y
y_high_target_b = y_low_target_b + y_high_source_b - y_low_source_b
# The same for the x coordinate.
x_low_source_r = -s_red_x
x_high_source_r = dim_x - s_red_x
x_low_target_r = 0
# If the shift reaches beyond the frame, reduce the copy area.
if x_low_source_r < 0:
x_low_target_r = -x_low_source_r
x_low_source_r = 0
if x_high_source_r > dim_x:
x_high_source_r = dim_x
x_high_target_r = x_low_target_r + x_high_source_r - x_low_source_r
x_low_source_b = -s_blue_x
x_high_source_b = dim_x - s_blue_x
x_low_target_b = 0
# If the shift reaches beyond the frame, reduce the copy area.
if x_low_source_b < 0:
x_low_target_b = -x_low_source_b
x_low_source_b = 0
if x_high_source_b > dim_x:
x_high_source_b = dim_x
x_high_target_b = x_low_target_b + x_high_source_b - x_low_source_b
# Now the coordinate bounds can be computed for which the output image has entries in all
# three channels. The green channel is not shifted and can just be copied in place.
min_y_g = max(y_low_target_r, y_low_target_b)
max_y_g = min(y_high_target_r, y_high_target_b)
min_x_g = max(x_low_target_r, x_low_target_b)
max_x_g = min(x_high_target_r, x_high_target_b)
# Allocate space for the output image (still not reduced in size), and copy the three color
# channels with the appropriate shifts applied.
output_image_interp = empty((max_y_g - min_y_g, max_x_g - min_x_g, 3), dtype=type)
output_image_interp[:, :, 0] = interp_image[min_y_g - s_red_y:max_y_g - s_red_y,
min_x_g - s_red_x:max_x_g - s_red_x, 0]
output_image_interp[:, :, 1] = interp_image[min_y_g:max_y_g, min_x_g:max_x_g, 1]
output_image_interp[:, :, 2] = interp_image[min_y_g - s_blue_y:max_y_g - s_blue_y,
min_x_g - s_blue_x:max_x_g - s_blue_x, 2]
# If output size reduction is specified, resize the intermediate image.
if reduce_output != 1:
output_image = resize(output_image_interp,
(round(output_image_interp.shape[1] / reduce_output),
round(output_image_interp.shape[0] / reduce_output)),
interpolation=INTER_CUBIC)
else:
output_image = output_image_interp
return output_image
@staticmethod
def measure_rgb_shift(image, channel_id, reference_id, max_shift, blur_strength=None):
"""
Measure the shift between two color channels of a three-color RGB image. Before measuring
the shift with cross correlation, a Gaussian blur can be applied to both channels to reduce
noise.
:param image: Input image (3 channel RGB).
:param channel_id: Index of the channel, the shift of which is to be measured against the
reference channel.
:param reference_id: Index of the reference channel, usually 1 for "green".
:param max_shift: Maximal search space radius in pixels.
:param blur_strength: Strength of the Gaussian blur to be applied first.
:return: Tuple (shift_y, shift_x) with coordinate shifts in y and x.
"""
# Test for invalid channel ids.
if not (0 <= channel_id <= 2):
raise ArgumentError("Invalid color channel id: " + str(channel_id))
if not (0 <= reference_id <= 2):
raise ArgumentError("Invalid reference channel id: " + str(reference_id))
# Reduce the size of the window for which the correlation will be computed to make space for
# the search space around it.
channel_window = image[max_shift:-max_shift, max_shift:-max_shift, channel_id]
channel_reference = image[:, :, reference_id]
# Apply Gaussian blur if requested.
if blur_strength:
channel_blurred = GaussianBlur(channel_window, (blur_strength, blur_strength), 0)
channel_reference_blurred = GaussianBlur(channel_reference,
(blur_strength, blur_strength), 0)
else:
channel_blurred = channel_window
channel_reference_blurred = channel_reference
# Compute the normalized cross correlation.
result = matchTemplate((channel_reference_blurred).astype(float32),
channel_blurred.astype(float32), TM_CCORR_NORMED)
# Determine the position of the maximum correlation, and compute the corresponding shifts.
minVal, maxVal, minLoc, maxLoc = minMaxLoc(result)
shift_y = max_shift - maxLoc[1]
shift_x = max_shift - maxLoc[0]
if abs(shift_y) != max_shift and abs(shift_x) != max_shift:
return (shift_y, shift_x)
else:
raise Error ("No matching shift found within given range")
@staticmethod
def insert_cross(frame, y_center, x_center, cross_half_len, color):
"""
Insert a colored cross into an image at a given location.
:param frame: Image into which the cross is to be inserted
:param y_center: y pixel coordinate of the center of the cross
:param x_center: x pixel coordinate of the center of the cross
:param cross_half_len: Extension of the cross from center in all four directions
:param color: Color, one out of "white", "red", "green" and "blue"
:return: -
"""
shape_y, shape_x = frame.shape[0:2]
if color == 'white':
rgb = [255, 255, 255]
elif color == 'red':
rgb = [255, 0, 0]
elif color == 'green':
rgb = [0, 255, 0]
elif color == 'blue':
rgb = [0, 0, 255]
elif color == 'cyan':
rgb = [0, 255, 255]
else:
rgb = [255, 255, 255]
# Be sure not to draw beyond frame boundaries.
if 0 <= x_center < shape_x:
for y in range(y_center - cross_half_len, y_center + cross_half_len + 1):
if 0 <= y < shape_y:
frame[y, x_center] = rgb
if 0 <= y_center < shape_y:
for x in range(x_center - cross_half_len, x_center + cross_half_len + 1):
if 0 <= x < shape_x:
frame[y_center, x] = rgb
@staticmethod
def compose_image(image_list, scale_factor=1, border=5):
"""
Arrange a list of monochrome images horizontally in a single image, with constant gaps in
between. If images are of different size, center-align them vertically. Optionally, the
resulting image can be re-scaled with the same factor in x and y directions.
:param image_list: List containing images (all of the same dtype)
:param scale_factor: Scaling factor for the resulting composite image
:param border: Width of black border and gaps between images
:return: Composite image of the same dtype as the image_list items
"""
shapes_y = [image.shape[0] for image in image_list]
shapes_x = [image.shape[1] for image in image_list]
max_shape_y = max(shapes_y)
sum_shape_x = sum(shapes_x)
# Check if all images of the list are of the same type.
type = image_list[0].dtype
for image in image_list[1:]:
if image.dtype != type:
raise ArgumentError("Trying to compose images of different types")
# Compute the dimensions of the composite image. The border and the gaps between images are
# 5 pixels wide.
composite_dim_y = max_shape_y + 2 * border
composite_dim_x = sum_shape_x + border * (len(image_list) + 1)
# Allocate the composite image.
composite = full((composite_dim_y, composite_dim_x), 0, dtype=type)
# Copy the images into the composite image.
x_pos = border
for index, image in enumerate(image_list):
y_pos = int((composite_dim_y - shapes_y[index]) / 2)
composite[y_pos: y_pos + shapes_y[index], x_pos: x_pos + shapes_x[index]] = image
x_pos += shapes_x[index] + border
# Return the resized composite image.
return resize(composite, None, fx=float(scale_factor), fy=float(scale_factor))
@staticmethod
def circle_around(y, x, r):
"""
Create an iterator which returns y, x pixel coordinates around a given position in an
image. Successive elements describe a circle around y, x with distance r (in the 1-norm).
The iterator ends when the full circle is traversed.
:param x: x coordinate of the center location
:param y: y coordinate of the center location
:param r: distance of the iterator items from position (y, x) (1-norm, in pixels)
:return: -
"""
# Special case 0: The only iterator element is the center point itself.
if r == 0:
yield (y, x)
# For the general case, set the first element and circle around (y, x) counter-clockwise.
j, i = y - r, x - r
while i < x + r:
i += 1
yield (j, i)
while j < y + r:
j += 1
yield (j, i)
while i > x - r:
i -= 1
yield (j, i)
while j > y - r:
j -= 1
yield (j, i)
@staticmethod
def write_video(name, frame_list, annotations, fps, depth=8):
"""
Create a video file from a list of frames.
:param name: File name of the video output
:param frame_list: List of frames to be written
:param annotations: List of text strings to be written into the corresponding frame
:param fps: Frames per second of video
:param depth: Bit depth of the image "frame", either 8 or 16.
:return: -
"""
# Delete the output file if it exists.
try:
unlink(name)
except:
pass
# Compute video frame size.
frame_height = frame_list[0].shape[0]
frame_width = frame_list[0].shape[1]
# Define the codec and create VideoWriter object
fourcc = VideoWriter_fourcc('D', 'I', 'V', 'X')
out = VideoWriter(name, fourcc, fps, (frame_width, frame_height))
# Define font for annotations.
font = FONT_HERSHEY_SIMPLEX
bottomLeftCornerOfText = (20, 50)
fontScale = 1
fontColor = (255, 255, 255)
fontThickness = 1
lineType = LINE_AA
# For each frame: If monochrome, convert it to three-channel color mode. insert annotation
# and write the frame.
for index, frame in enumerate(frame_list):
# If the frames are 16bit, convert them to 8bit.
if depth == 8:
frame_8b = frame
else:
frame_8b = (frame / 255.).astype(uint8)
if len(frame.shape) == 2:
rgb_frame = stack((frame_8b,) * 3, -1)
else:
rgb_frame = frame_8b
putText(rgb_frame, annotations[index],
bottomLeftCornerOfText,
font,
fontScale,
fontColor,
fontThickness,
lineType)
out.write(rgb_frame)
out.release()
@staticmethod
def post_process(image, layers):
"""
Apply all postprocessing layers to the input image "image". If the image is in color mode,
the postprocessing is computed either in BGR mode or on the luminance channel only. All
computations are performed in 32bit floating point mode.
:param image: Input image, either BGR or Grayscale (16bit uint).
:param layers: postprocessing layers with all parameters.
:return: Processed image in the same 16bit uint format as the input image.
"""
# Check if the original image is selected (version 0). In this case nothing is to be done.
if not layers:
return image.astype(uint16)
# Convert the image to 32bit floating point format.
input_image = image.astype(float32)
color = len(image.shape) == 3
# If the luminance channel is to be processed only, extract the luminance channel.
if color and layers[0].luminance_only:
input_image_hsv = cvtColor(input_image, COLOR_BGR2HSV)
layer_input = input_image_hsv[:, :, 2]
else:
layer_input = input_image
# Go through all layers and apply the sharpening filters.
for layer_index, layer in enumerate(layers):
# Bilateral filter is needed:
if abs(layer.bi_fraction) > 1.e-5:
layer_bilateral = bilateralFilter(layer_input, 0, layer.bi_range * 256.,
layer.radius / 3., borderType=BORDER_DEFAULT)
# Gaussian filter is needed:
if abs(layer.bi_fraction - 1.) > 1.e-5:
layer_gauss = GaussianBlur(layer_input, (0, 0), layer.radius / 3.,
borderType=BORDER_DEFAULT)
# Compute the input for the next layer. First case: bilateral only.
if abs(layer.bi_fraction - 1.) <= 1.e-5:
next_layer_input = layer_bilateral
# Case Gaussian only.
elif abs(layer.bi_fraction) <= 1.e-5:
next_layer_input = layer_gauss
# Mixed case.
else:
next_layer_input = layer_bilateral * layer.bi_fraction + \
layer_gauss * (1. - layer.bi_fraction)
layer_component_before_denoise = layer_input - next_layer_input
# If denoising is chosen for this layer, apply a Gaussian filter.
if layer.denoise > 1.e-5:
layer_component = GaussianBlur(layer_component_before_denoise, (0, 0),
layer.radius / 3., borderType=BORDER_DEFAULT) * layer.denoise + \
layer_component_before_denoise * (1. - layer.denoise)
else:
layer_component = layer_component_before_denoise
# Accumulate the contributions from all layers. On the first layer initialize the
# summation buffer.
if layer_index:
components_accumulated += layer_component * layer.amount
else:
components_accumulated = layer_component * layer.amount
layer_input = next_layer_input
# After all layers are accumulated, finally add the maximally blurred image.
components_accumulated += next_layer_input
# Reduce the value range so that they fit into 16bit uint, and convert to uint16. In case
# the luminance channel was processed only, insert the processed luminance channel into the
# HSV representation of the original image, and convert back to BGR.
if color and layers[0].luminance_only:
input_image_hsv[:, :, 2] = components_accumulated
return cvtColor(input_image_hsv.clip(min=0., max=65535.), COLOR_HSV2BGR).astype(uint16)
else:
return components_accumulated.clip(min=0., max=65535.).astype(uint16)
@staticmethod
def gaussian_sharpen(input_image, amount, radius, luminance_only=False):
"""
Sharpen an image with a Gaussian kernel. The input image can be B/W or color.
:param input_image: Input image, type uint16
:param amount: Amount of sharpening
:param radius: Radius of Gaussian kernel (in pixels)
:param luminance_only: True, if only the luminance channel of a color image is to be
sharpened. Default is False.
:return: The sharpened image (B/W or color, as input), type uint16
"""
color = len(input_image.shape) == 3
# Translate the kernel radius into standard deviation.
sigma = radius / 3
# Convert the image to floating point format.
image = input_image.astype(float32)
# Special case: Only sharpen the luminance channel of a color image.
if color and luminance_only:
hsv = cvtColor(image, COLOR_BGR2HSV)
luminance = hsv[:, :, 2]
# Apply a Gaussian blur filter, subtract it from the original image, and add a multiple
# of this correction to the original image. Clip values out of range.
luminance_blurred = GaussianBlur(luminance, (0, 0), sigma, borderType=BORDER_DEFAULT)
hsv[:, :, 2] = (luminance + amount * (luminance - luminance_blurred)).clip(min=0.,
max=65535.)
# Convert the image back to uint16.
return cvtColor(hsv, COLOR_HSV2BGR).astype(uint16)
# General case: Treat the entire image (B/W or color 16bit mode).
else:
image_blurred = GaussianBlur(image, (0, 0), sigma, borderType=BORDER_DEFAULT)
return (image + amount * (image - image_blurred)).clip(min=0., max=65535.).astype(
uint16)
@staticmethod
def gaussian_blur(input_image, amount, radius, luminance_only=False):
"""
Soften an image with a Gaussian kernel. The input image can be B/W or color.
:param input_image: Input image, type uint16
:param amount: Amount of blurring, between 0. and 1.
:param radius: Radius of Gaussian kernel (in pixels)
:param luminance_only: True, if only the luminance channel of a color image is to be
blurred. Default is False.
:return: The blurred image (B/W or color, as input), type uint16
"""
color = len(input_image.shape) == 3
# Translate the kernel radius into standard deviation.
sigma = radius / 3
# Convert the image to floating point format.
image = input_image.astype(float32)
# Special case: Only blur the luminance channel of a color image.
if color and luminance_only:
hsv = cvtColor(image, COLOR_BGR2HSV)
luminance = hsv[:, :, 2]
# Apply a Gaussian blur filter, subtract it from the original image, and add a multiple
# of this correction to the original image. Clip values out of range.
luminance_blurred = GaussianBlur(luminance, (0, 0), sigma, borderType=BORDER_DEFAULT)
hsv[:, :, 2] = (luminance_blurred*amount + luminance*(1.-amount)).clip(min=0.,
max=65535.)
# Convert the image back to uint16.
return cvtColor(hsv, COLOR_HSV2BGR).astype(uint16)
# General case: Treat the entire image (B/W or color 16bit mode).
else:
image_blurred = GaussianBlur(image, (0, 0), sigma, borderType=BORDER_DEFAULT)
return (image_blurred*amount + image*(1.-amount)).clip(min=0., max=65535.).astype(
uint16)
@staticmethod
def wavelet_sharpen(input_image, amount, radius):
"""
Sharpen a B/W or color image with wavelets. The underlying algorithm was taken from the
Gimp wavelet plugin, originally written in C and published under the GPLv2+ license at:
https://github.com/mrossini-ethz/gimp-wavelet-sharpen/blob/master/src/wavelet.c
:param input_image: Input image (B/W or color), type uint16
:param amount: Amount of sharpening
:param radius: Radius in pixels
:return: Sharpened image, same format as input image
"""
height, width = input_image.shape[:2]
color = len(input_image.shape) == 3
# Allocate workspace: Three complete images, plus 1D object with length max(row, column).
if color:
fimg = empty((3, height, width, 3), dtype=float32)
temp = zeros((max(width, height), 3), dtype=float32)
else:
fimg = empty((3, height, width), dtype=float32)
temp = zeros(max(width, height), dtype=float32)
# Convert input image to floats.
fimg[0] = input_image / 65535
# Start with level 0. Store its Laplacian on level 1. The operator is separated in a
# column and a row operator.
hpass = 0
for lev in range(5):
# Highpass and lowpass levels use image indices 1 and 2 in alternating mode to save
# space.
lpass = ((lev & 1) + 1)
if color:
for row in range(height):
Miscellaneous.mexican_hat_color(temp, fimg[hpass][row, :, :], width, 1 << lev)
fimg[lpass][row, :, :] = temp[:width, :] * 0.25
for col in range(width):
Miscellaneous.mexican_hat_color(temp, fimg[lpass][:, col, :], height, 1 << lev)
fimg[lpass][:, col, :] = temp[:height, :] * 0.25
else:
for row in range(height):
Miscellaneous.mexican_hat(temp, fimg[hpass][row, :], width, 1 << lev)
fimg[lpass][row, :] = temp[:width] * 0.25
for col in range(width):
Miscellaneous.mexican_hat(temp, fimg[lpass][:, col], height, 1 << lev)
fimg[lpass][:, col] = temp[:height] * 0.25
# Compute the amount of the correction at the current level.
amt = amount * exp(-(lev - radius) * (lev - radius) / 1.5) + 1.
fimg[hpass] -= fimg[lpass]
fimg[hpass] *= amt
# Accumulate all corrections in the first workspace image.
if hpass:
fimg[0] += fimg[hpass]
hpass = lpass
# At the end add the coarsest level and convert back to 16bit integer format.
fimg[0] = ((fimg[0] + fimg[lpass]) * 65535.).clip(min=0., max=65535.)
return fimg[0].astype(uint16)
@staticmethod
def mexican_hat(temp, base, size, sc):
"""
Apply a 1D strided second derivative to a row or column of a B/W image. Store the result
in the temporary workspace "temp".
:param temp: Workspace (type float32), length at least "size" elements
:param base: Input image (B/W), Type float32
:param size: Length of image row / column
:param sc: Stride (power of 2) of operator
:return: -
"""
# Special case at begin of row/column. Full operator not applicable.
temp[:sc] = 2 * base[:sc] + base[sc:0:-1] + base[sc:2 * sc]
# Apply the full operator.
temp[sc:size - sc] = 2 * base[sc:size - sc] + base[:size - 2 * sc] + base[2 * sc:size]
# Special case at end of row/column. The full operator is not applicable.
temp[size - sc:size] = 2 * base[size - sc:size] + base[size - 2 * sc:size - sc] + \
base[size - 2:size - 2 - sc:-1]
@staticmethod
def mexican_hat_color(temp, base, size, sc):
"""
Apply a 1D strided second derivative to a row or column of a color image. Store the result
in the temporary workspace "temp".
:param temp: Workspace (type float32), length at least "size" elements (first dimension)
times 3 colors (second dimension).
:param base: Input image (color), Type float32
:param size: Length of image row / column
:param sc: Stride (power of 2) of operator
:return: -
"""
# Special case at begin of row/column. Full operator not applicable.
temp[:sc, :] = 2 * base[:sc, :] + base[sc:0:-1, :] + base[sc:2 * sc, :]
# Apply the full operator.
temp[sc:size - sc, :] = 2 * base[sc:size - sc, :] + base[:size - 2 * sc, :] + base[
2 * sc:size, :]
# Special case at end of row/column. The full operator is not applicable.
temp[size - sc:size, :] = 2 * base[size - sc:size, :] + base[size - 2 * sc:size - sc, :] + \
base[size - 2:size - 2 - sc:-1, :]
@staticmethod
def protocol(string, logfile, precede_with_timestamp=True):
"""
Print a message (optionally plus time stamp) to standard output. If it is requested to store
a logfile with the stacked image, write the string to that file as well.
:param string: Message to be printed after the time stamp
:param logfile: logfile or None (no logging)
:return: -
"""
# Precede the text with a time stamp and print it to stdout. Note that stdout may be
# redirected to a file.
if precede_with_timestamp:
output_string = '{0} {1}'.format(datetime.now().strftime("%H-%M-%S.%f")[:-5], string)
else:
output_string = string
print(output_string)
stdout.flush()
# If a logfile per stacked image was requested, write the string to that file as well.
if logfile:
logfile.write(output_string + "\n")
@staticmethod
def print_stacking_parameters(configuration, logfile):
"""
Print a table with all stacking parameters.
:param configuration: Object holding all configuration parameters.
:param logfile: logfile or None (no logging)
:return: -
"""
# Compile all parameters and their values to be printed.
if configuration.global_parameters_maximum_memory_active:
parameters = [["RAM limit set explicitly (GBytes)",
str(configuration.global_parameters_maximum_memory_amount)]]
else:
parameters = [["RAM limit set explicitly", "False"]]
if configuration.global_parameters_buffering_level == -1:
parameters += [["Buffering level", "auto"]]
else:
parameters += [["Buffering level set explicitly",
str(configuration.global_parameters_buffering_level)]]
parameters += [["Debayering default", configuration.frames_debayering_default],
["Debayering method", configuration.frames_debayering_method],
["Noise level (add Gaussian blur)", str(configuration.frames_gauss_width)],
["Frame stabilization mode", configuration.align_frames_mode]]
# The following parameters are only active in "Surface" mode.
if configuration.align_frames_mode == "Surface":
parameters += [
["Automatic frame stabilization", str(configuration.align_frames_automation)],
["Stabilization patch size (% of frame size)",
str(int(round(100. / configuration.align_frames_rectangle_scale_factor)))],
["Stabilization search width (pixels)",
str(configuration.align_frames_search_width)]]
# Continue with general parameters.
parameters += [["Percentage of best frames for reference frame computation",
str(configuration.align_frames_average_frame_percent)],
["Object is changing fast (e.g. Jupiter, Sun)",
str(configuration.align_frames_fast_changing_object)],
["Alignment box width (pixels)",
str(2 * configuration.alignment_points_half_box_width)],
["Max. alignment search width (pixels)",
str(configuration.alignment_points_search_width)], ["Minimum structure",
str(configuration.alignment_points_structure_threshold)],
["Minimum brightness",
str(configuration.alignment_points_brightness_threshold)]]
# The stack size is defined either as frame number or percentage.
if configuration.alignment_points_frame_percent > 0:
parameters += [["Percentage of best frames to be stacked",
str(configuration.alignment_points_frame_percent)],
["Normalize frame brightness", str(configuration.frames_normalization)]]
else:
parameters += [["Number of best frames to be stacked",
str(configuration.alignment_points_frame_number)],
["Normalize frame brightness", str(configuration.frames_normalization)]]
# If brightness normalization is checked, add the black cut-off value.
if configuration.frames_normalization:
parameters += [
["Normalization black cut-off", str(configuration.frames_normalization_threshold)]]
# If drizzling is active, add the factor.
if configuration.stack_frames_drizzle_factor_string != "Off":
parameters += [["Drizzle factor in stacking",
str(configuration.stack_frames_drizzle_factor_string)]]
output_string = "\n Stacking parameters: " \
" | Value |\n" \
" " \
"---------------------------------------------------------------------------------------------" \
"\n "
# Extend the output string with a line for every parameter to be printed.
for line in parameters:
output_string += " {0:60s} | {1:29s}|\n ".format(line[0], line[1])
# Write the complete table.
Miscellaneous.protocol(output_string, logfile, precede_with_timestamp=False)
@staticmethod
def print_postproc_parameters(postproc_version, logfile):
"""
Print a table with postprocessing layer info for the selected postprocessing version.
:param postproc_version: Object holding postprocessing parameters for the selected version.
:param logfile: logfile or None (no logging)
:return: -
"""
# Test if an RGB correction has been applied.
if postproc_version.shift_red != (0., 0.) or postproc_version.shift_blue != (0., 0.):
# Find out if the RGB correction was done automatically.
if postproc_version.rgb_automatic:
intro = " Automatic RGB correction, "
else:
intro = " Manual RGB correction, "
(shift_red_y, shift_red_x) = postproc_version.shift_red
(shift_blue_y, shift_blue_x) = postproc_version.shift_blue
n_digits = [0, 1, 2][postproc_version.rgb_resolution_index]
if shift_red_y >= 0.:
dir_red_y = " pixels down"
else:
dir_red_y = " pixels up"
if shift_red_x >= 0.:
dir_red_x = " pixels right"
else:
dir_red_x = " pixels left"
if shift_blue_y >= 0.:
dir_blue_y = " pixels down"
else:
dir_blue_y = " pixels up"
if shift_blue_x >= 0.:
dir_blue_x = " pixels right"
else:
dir_blue_x = " pixels left"
# Special case 0 digits: In this case the number of digits must be omitted.
# If the round function is called with "n_digits=0", the result still has one digit
# after the decimal point.
if n_digits:
Miscellaneous.protocol(
intro + "red channel shifted " +
str(round(abs(shift_red_y), n_digits)) + dir_red_y + ", " +
str(round(abs(shift_red_x), n_digits)) + dir_red_x + ", blue channel shifted " +
str(round(abs(shift_blue_y), n_digits)) + dir_blue_y + ", " +
str(round(abs(shift_blue_x), n_digits)) + dir_blue_x + ".",
logfile, precede_with_timestamp=False)
else:
Miscellaneous.protocol(
intro + "red channel shifted " +
str(round(abs(shift_red_y))) + dir_red_y + ", " +
str(round(abs(shift_red_x))) + dir_red_x + ", blue channel shifted " +
str(round(abs(shift_blue_y))) + dir_blue_y + ", " +
str(round(abs(shift_blue_x))) + dir_blue_x + ".",
logfile, precede_with_timestamp=False)
if postproc_version.layers:
output_string = " Postprocessing method: " + postproc_version.layers[0].postproc_method + "\n\n" + \
" Layer | Radius | Amount | Bi fraction (%) | Bi range | Denoise (%) | Luminance only |\n" \
" ------------------------------------------------------------------------------------------------------------------" \
"\n "
# Extend the three table lines up to the max index.
for index, layer in enumerate(postproc_version.layers):
output_string += " {0:3d} | {1:5.2f} | {2:6.2f} | {3:4.0f} | {4:5.1f} | {5:4.0f} | {6:8s} |" \
"\n ".format(index + 1, layer.radius, layer.amount, layer.bi_fraction*100., layer.bi_range, layer.denoise*100., str(layer.luminance_only))
Miscellaneous.protocol(output_string, logfile, precede_with_timestamp=False)
if __name__ == "__main__":
# Test program for routine search_local_match
# Set the size of the image frame.
frame_height = 960
frame_width = 1280
# Initialize the frame with a wave-like pattern in x and y directions.
x_max = 30.
x_vec = arange(0., x_max, x_max / frame_width)
y_max = 25.
y_vec = arange(0., y_max, y_max / frame_height)
frame = empty((frame_height, frame_width))
for y_j, y in enumerate(y_vec):
for x_i, x in enumerate(x_vec):
frame[y_j, x_i] = sin(y) * sin(x)
# Set the size and location of the reference frame window and cut it out from the frame.
window_height = 40
window_width = 50
reference_y_low = 400
reference_x_low = 500
reference_box = frame[reference_y_low:reference_y_low + window_height,
reference_x_low:reference_x_low + window_width]
# Set the true displacement vector to be checked against the result of the search function.
displacement_y = 1
displacement_x = -2
# The start point for the local search is offset from the true matching point.
y_low = reference_y_low + displacement_y
y_high = y_low + window_height
x_low = reference_x_low + displacement_x
x_high = x_low + window_width
# Set the radius of the search area.
search_width = 20
sampling_stride = 1
dev_table = empty((2 * search_width + 1, 2 * search_width + 1), dtype=float32)
# First try the standard method "search_local_match". Compute the displacement vector, and
# print a comparison of the true and computed values.
start = time()
rep_count = 1
for iter in range(rep_count):
[dy, dx], dev_r = Miscellaneous.search_local_match(reference_box, frame, y_low, y_high,
x_low, x_high, search_width,
sampling_stride, sub_pixel=False)
end = time()
print("Standard method, true displacements: " + str([displacement_y, displacement_x]) +
", computed: " + str([dy, dx]) + ", execution time (s): " + str((end - start) / rep_count))
# As a comparison, do the same using the steepest descent method.
start = time()
for iter in range(rep_count):
[dy, dx], dev_r = Miscellaneous.search_local_match_gradient(reference_box, frame,
y_low, y_high,
x_low, x_high, search_width,
sampling_stride, dev_table)
end = time()
print("steepest descent, true displacements: " + str([displacement_y, displacement_x]) +
", computed: " + str([dy, dx]) + ", execution time (s): " + str((end - start) / rep_count))
# Now test the alternative method in comparison to the straight-forward one. First,
# compute the reference frame box stack. The initialization has to be performed only once for
# each alignment point. It can be reused for all frames containing the AP.
start = time()
for iter in range(rep_count):
reference_stack, displacements, radius_start = \
Miscellaneous.search_local_match_init(frame, reference_y_low,
reference_y_low + window_height, reference_x_low,
reference_x_low + window_width, search_width)
end = time()
print("Match initialization time (s): " + str((end - start) / rep_count))
# Compute the displacement. The result should be the same as above.
start = time()
for iter in range(rep_count):
[dy, dx], dev_r = Miscellaneous.search_local_match_execute(
frame[y_low:y_high, x_low:x_high], reference_stack, displacements,
radius_start)
end = time()
print("Match execution, true displacements: " + str(
[displacement_y, displacement_x]) + ", computed: " + str(
[dy, dx]) + ", execution time (s): " + str((end - start) / rep_count))
|