1 // Copyright (C) 2020 by Yuri Victorovich. All rights reserved.
2
3 #include "data-table-2d.h"
4 #include "tensor.h"
5 #include "misc.h"
6 #include "util.h"
7
8 #include <QFontMetrics>
9 #include <QImage>
10 #include <QPixmap>
11 #include <QSettings>
12
13 #include <cmath>
14 #include <functional>
15 #include <sstream>
16 #include <tuple>
17
18 #include <assert.h>
19 #include <half.hpp> // to instantiate DataTable2D with the float16 type
20
21 /// template helpers
22
23 namespace Helpers {
24 template<typename T>
25 struct CTypeToQtType {
C2QtHelpers::CTypeToQtType26 static T C2Qt(T t) {return t;}
27 };
28 template<>
29 struct CTypeToQtType<int64_t> {
C2QtHelpers::CTypeToQtType30 static qlonglong C2Qt(int64_t t) {return (qlonglong)t;}
31 };
32 }
33
34 /// DataTable2DBase
35
DataTable2DBase(QWidget * parent)36 DataTable2DBase::DataTable2DBase(QWidget *parent)
37 : QWidget(parent)
38 { }
39
~DataTable2DBase()40 DataTable2DBase::~DataTable2DBase()
41 { }
42
43 /// local helper classes
44
45 template<typename T>
46 class DataSource {
47 unsigned numRows;
48 unsigned numColumns;
49 public:
DataSource(unsigned numRows_,unsigned numColumns_)50 DataSource(unsigned numRows_, unsigned numColumns_)
51 : numRows(numRows_)
52 , numColumns(numColumns_)
53 {
54 }
~DataSource()55 virtual ~DataSource() { }
56
57 public: // iface: data access
nrows() const58 unsigned nrows() const {
59 return numRows;
60 }
ncols() const61 unsigned ncols() const {
62 return numColumns;
63 }
64 virtual T value(unsigned r, unsigned c) const = 0;
65
66 public: // iface: data computations
computeMinMax() const67 std::tuple<T,T> computeMinMax() const {
68 T dmin = std::numeric_limits<T>::max();
69 T dmax = std::numeric_limits<T>::lowest();
70 for (unsigned r = 0, re = nrows(); r < re; r++)
71 for (unsigned c = 0, ce = ncols(); c < ce; c++) {
72 auto d = value(r,c);
73 if (d < dmin)
74 dmin = d;
75 if (d > dmax)
76 dmax = d;
77 }
78 return std::tuple<T,T>(dmin, dmax);
79 }
80 };
81
82 template<typename T>
83 class TensorSliceDataSource : public DataSource<T> {
84 const TensorShape shape;
85 const T* &data; // shares data pointer with the DataTable2D instance
86 unsigned idxVertical;
87 unsigned idxHorizontal;
88 mutable std::vector<unsigned> fixedIdxs;
89
90 public:
TensorSliceDataSource(const TensorShape & shape_,unsigned idxVertical_,unsigned idxHorizontal_,const std::vector<unsigned> & fixedIdxs_,const T * & data_)91 TensorSliceDataSource(const TensorShape &shape_, unsigned idxVertical_, unsigned idxHorizontal_, const std::vector<unsigned> &fixedIdxs_, const T *&data_)
92 : DataSource<T>(shape_[idxVertical_], shape_[idxHorizontal_])
93 , shape(shape_)
94 , data(data_)
95 , idxVertical(idxVertical_)
96 , idxHorizontal(idxHorizontal_)
97 , fixedIdxs(fixedIdxs_)
98 {
99 }
100
value(unsigned r,unsigned c) const101 T value(unsigned r, unsigned c) const override {
102 fixedIdxs[idxVertical] = r;
103 fixedIdxs[idxHorizontal] = c;
104 return data[offset(fixedIdxs, shape)];
105 }
106
107 private:
offset(std::vector<unsigned> & idxs,const TensorShape & shape)108 static unsigned offset(std::vector<unsigned> &idxs, const TensorShape &shape) {
109 unsigned off = 0;
110 for (unsigned i = 0, ie = shape.size(); i < ie; i++) {
111 off *= shape[i];
112 off += idxs[i];
113 }
114 return off;
115 }
116 };
117
118 enum ColorSchemaEnum {
119 COLORSCHEME_NONE,
120 COLORSCHEME_BLUE_TO_RED,
121 COLORSCHEME_RED_TO_BLUE,
122 COLORSCHEME_GRAYSCALE_UP,
123 COLORSCHEME_GRAYSCALE_DOWN,
124 COLORSCHEME_GRAYSCALE_ZERO_UP,
125 COLORSCHEME_GRAYSCALE_ZERO_DOWN,
126 COLORSCHEME_GRAYSCALE_MIDDLE_UP,
127 COLORSCHEME_GRAYSCALE_MIDDLE_DOWN
128 };
129
130 class BaseColorSchema {
131 public:
~BaseColorSchema()132 virtual ~BaseColorSchema() { }
133 virtual QColor color(float value) const = 0;
134 };
135
136 template<class ColorMapper, class ValueMapper>
137 class ColorSchema : public BaseColorSchema {
138 float minValue;
139 float maxValue;
140 public:
ColorSchema(float minValue_,float maxValue_)141 ColorSchema(float minValue_, float maxValue_)
142 : minValue(minValue_)
143 , maxValue(maxValue_)
144 {
145 }
~ColorSchema()146 ~ColorSchema() { }
147
148 public:
color(float value) const149 QColor color(float value) const override {
150 return ColorMapper::mapColor(ValueMapper::mapValue(value, minValue, maxValue));
151 }
152 };
153
154 class ValueMapperLinear {
155 public:
mapValue(float value,float minValue,float maxValue)156 static float mapValue(float value, float minValue, float maxValue) {
157 return (value-minValue)/(maxValue-minValue);
158 }
159 };
160 class ValueMapperFromZero {
161 public:
mapValue(float value,float minValue,float maxValue)162 static float mapValue(float value, float minValue, float maxValue) {
163 return ValueMapperLinear::mapValue(std::abs(value), 0, std::max(std::abs(minValue), std::abs(maxValue)));
164 }
165 };
166 class ValueMapperFromMiddle {
167 public:
mapValue(float value,float minValue,float maxValue)168 static float mapValue(float value, float minValue, float maxValue) {
169 float mid = (minValue+maxValue)/2;
170 return ValueMapperLinear::mapValue(value < mid ? mid - value : value, mid, maxValue);
171 }
172 };
173
174 template<uint8_t Clr1R, uint8_t Clr1G, uint8_t Clr1B, uint8_t Clr2R, uint8_t Clr2G, uint8_t Clr2B>
175 class ColorMapper {
176 public:
mapColor(float value)177 static QColor mapColor(float value) {
178 return QColor(
179 float(Clr1R)+value*(Clr2R-Clr1R),
180 float(Clr1G)+value*(Clr2G-Clr1G),
181 float(Clr1B)+value*(Clr2B-Clr1B)
182 );
183 }
184 };
185
186 class TextColorForBackground {
187 public:
color(QColor clr)188 static QColor color(QColor clr) {
189 return clr.valueF() > 0.7 ? Qt::black : Qt::white;
190 }
191 };
192
createColorSchema(ColorSchemaEnum colorSchema,float minValue,float maxValue)193 static const BaseColorSchema* createColorSchema(ColorSchemaEnum colorSchema, float minValue, float maxValue) {
194 #define B 0 // "black"
195 #define W 255 // "white"
196 switch (colorSchema) {
197 case COLORSCHEME_NONE:
198 return nullptr;
199 case COLORSCHEME_BLUE_TO_RED:
200 return new ColorSchema<ColorMapper<B,B,W, W,B,B>, ValueMapperLinear>(minValue, maxValue);
201 case COLORSCHEME_RED_TO_BLUE:
202 return new ColorSchema<ColorMapper<W,B,B, B,B,W>, ValueMapperLinear>(minValue, maxValue);
203 case COLORSCHEME_GRAYSCALE_UP:
204 return new ColorSchema<ColorMapper<B,B,B, W,W,W>, ValueMapperLinear>(minValue, maxValue);
205 case COLORSCHEME_GRAYSCALE_DOWN:
206 return new ColorSchema<ColorMapper<W,W,W, B,B,B>, ValueMapperLinear>(minValue, maxValue);
207 case COLORSCHEME_GRAYSCALE_ZERO_UP:
208 return new ColorSchema<ColorMapper<B,B,B, W,W,W>, ValueMapperFromZero>(minValue, maxValue);
209 case COLORSCHEME_GRAYSCALE_ZERO_DOWN:
210 return new ColorSchema<ColorMapper<W,W,W, B,B,B>, ValueMapperFromZero>(minValue, maxValue);
211 case COLORSCHEME_GRAYSCALE_MIDDLE_UP:
212 return new ColorSchema<ColorMapper<B,B,B, W,W,W>, ValueMapperFromMiddle>(minValue, maxValue);
213 case COLORSCHEME_GRAYSCALE_MIDDLE_DOWN:
214 return new ColorSchema<ColorMapper<W,W,W, B,B,B>, ValueMapperFromMiddle>(minValue, maxValue);
215 }
216 #undef B
217 #undef W
218 }
219
220 template<typename T>
221 class DataModel : public QAbstractTableModel {
222 std::unique_ptr<const DataSource<T>> dataSource;
223 std::unique_ptr<const BaseColorSchema> colorSchema;
224 public:
DataModel(const DataSource<T> * dataSource_,const BaseColorSchema * colorSchema_,QObject * parent)225 DataModel(const DataSource<T> *dataSource_, const BaseColorSchema *colorSchema_, QObject *parent)
226 : QAbstractTableModel(parent)
227 , dataSource(dataSource_)
228 , colorSchema(colorSchema_)
229 {
230 }
231
232 public: // QAbstractTableModel interface implementation
rowCount(const QModelIndex & parent=QModelIndex ()) const233 int rowCount(const QModelIndex &parent = QModelIndex()) const override {
234 return dataSource->nrows();
235 }
columnCount(const QModelIndex & parent=QModelIndex ()) const236 int columnCount(const QModelIndex &parent = QModelIndex()) const override {
237 return dataSource->ncols();
238 }
data(const QModelIndex & index,int role) const239 QVariant data(const QModelIndex &index, int role) const override {
240 switch (role) {
241 case Qt::DisplayRole: // text
242 return QVariant(Helpers::CTypeToQtType<T>::C2Qt(dataSource->value(index.row(), index.column())));
243 case Qt::BackgroundRole: // background
244 return colorSchema ? colorSchema->color(dataSource->value(index.row(), index.column())) : QVariant();
245 case Qt::ForegroundRole: // text color
246 return colorSchema ? TextColorForBackground::color(colorSchema->color(dataSource->value(index.row(), index.column()))) : QVariant();
247 default:
248 return QVariant();
249 }
250 }
251
252 public: // custom interface
setDataSource(const DataSource<T> * dataSource_)253 void setDataSource(const DataSource<T> *dataSource_) {
254 beginResetModel();
255 dataSource.reset(dataSource_);
256 endResetModel();
257 }
setColorSchema(const BaseColorSchema * colorSchema_)258 void setColorSchema(const BaseColorSchema *colorSchema_) {
259 beginResetModel();
260 colorSchema.reset(colorSchema_);
261 endResetModel();
262 }
getDataSource() const263 const DataSource<T>* getDataSource() const {
264 return dataSource.get();
265 }
beginResetModel()266 void beginResetModel() {
267 QAbstractTableModel::beginResetModel();
268 }
endResetModel()269 void endResetModel() {
270 QAbstractTableModel::endResetModel();
271 }
272 };
273
274 /// DataTable2D
275
276 template<typename T>
DataTable2D(const TensorShape & shape_,const T * data_,QWidget * parent)277 DataTable2D<T>::DataTable2D(const TensorShape &shape_, const T *data_, QWidget *parent)
278 : DataTable2DBase(parent)
279 , shape(shape_)
280 , data(data_)
281 , dimVertical(0)
282 , dimHorizontal(0)
283 , self(false)
284 , layout(this)
285 , headerWidget(this)
286 , headerLayout(&headerWidget)
287 , shapeLabel(QString(tr("Shape: %1")).arg(S2Q(STR(shape))), &headerWidget)
288 , shapeDimensionsWidget(&headerWidget)
289 , shapeDimensionsLayout(&shapeDimensionsWidget)
290 , dataRangeLabel(&headerWidget)
291 , colorSchemaLabel(tr("Color scheme:"), &headerWidget)
292 , colorSchemaComboBox(&headerWidget)
293 , header1Widget(this)
294 , header1Layout(&header1Widget)
295 , filler1Widget(&header1Widget)
296 , scaleBwImageLabel(tr("Scale image"), &header1Widget)
297 , scaleBwImageSpinBox(&header1Widget)
298 , viewDataAsBwImageCheckBox(tr("View Data as B/W Image"), &header1Widget)
299 , dataViewStackWidget(this)
300 , tableView(&dataViewStackWidget)
301 , imageViewScrollArea(&dataViewStackWidget)
302 , imageView(&imageViewScrollArea)
303 , imageViewInitialized(false)
304 {
305 assert(shape.size() > 1); // otherwise DataTable1D should be used
306
307 layout.addWidget(&headerWidget);
308 headerLayout.addWidget(&shapeLabel);
309 headerLayout.addWidget(&shapeDimensionsWidget);
310 headerLayout.addWidget(&dataRangeLabel);
311 headerLayout.addWidget(&colorSchemaLabel);
312 headerLayout.addWidget(&colorSchemaComboBox);
313 layout.addWidget(&header1Widget);
314 header1Layout.addWidget(&filler1Widget);
315 header1Layout.addWidget(&scaleBwImageLabel);
316 header1Layout.addWidget(&scaleBwImageSpinBox);
317 header1Layout.addWidget(&viewDataAsBwImageCheckBox);
318 layout.addWidget(&dataViewStackWidget);
319 dataViewStackWidget.insertWidget(0, &tableView);
320 dataViewStackWidget.insertWidget(1, &imageViewScrollArea);
321 imageViewScrollArea.setWidget(&imageView);
322
323 { // create comboboxes for shape dimensions
324 unsigned numMultiDims = Tensor::numMultiDims(shape);
325 //std::vector<bool> multiDims = tensorGetMultiDims(shape);
326 unsigned dim = 0;
327 for (auto d : shape) {
328 auto combobox = new QComboBox(&shapeDimensionsWidget);
329 combobox->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); // (H) The sizeHint() is a maximum (V) The sizeHint() is a maximum
330 shapeDimensionsLayout.addWidget(combobox);
331 if (d == 1) { // dimensions=1 are excluded from selection
332 combobox->addItem(tr("single 1"));
333 combobox->setEnabled(false);
334 } else {
335 combobox->addItem(tr("--X (columns)--"));
336 combobox->addItem(tr("--Y (rows)--"));
337 if (numMultiDims > 2) {
338 for (unsigned index = 0; index < d; index++)
339 combobox->addItem(QString(tr("index: %1")).arg(index+1));
340 }
341 }
342 connect(combobox, QOverload<int>::of(&QComboBox::activated), [this,dim](int index) {
343 auto swapXY = [this,dim](unsigned &dimMy, unsigned &dimOther, bool iAmY) {
344 unsigned otherDim = dimOther;
345 dimMy = dimOther;
346 dimOther = dim;
347 shapeDimensionsComboBoxes[otherDim]->setCurrentIndex(iAmY ? 1/*Y*/ : 0/*X*/);
348 (static_cast<DataModel<T>*>(tableModel.get()))->setDataSource(new TensorSliceDataSource(shape, dimVertical, dimHorizontal, mkIdxs(), data));
349 };
350 auto changeXYtoIndex = [this](unsigned &dimChanged, bool iAmY) {
351 // choose another X/Y
352 for (unsigned dim = 0; dim < shape.size(); dim++)
353 if (shape[dim] > 1 && dim!=dimVertical && dim!=dimHorizontal) {
354 dimChanged = dim;
355 shapeDimensionsComboBoxes[dim]->setCurrentIndex(iAmY ? 1/*Y*/ : 0/*X*/);
356 break;
357 }
358 (static_cast<DataModel<T>*>(tableModel.get()))->setDataSource(new TensorSliceDataSource(shape, dimVertical, dimHorizontal, mkIdxs(), data));
359 };
360 auto changeIndexToXY = [this,dim](unsigned &dimChanged) {
361 shapeDimensionsComboBoxes[dimChanged]->setCurrentIndex(2/*index=1*/);
362 dimChanged = dim;
363 (static_cast<DataModel<T>*>(tableModel.get()))->setDataSource(new TensorSliceDataSource(shape, dimVertical, dimHorizontal, mkIdxs(), data));
364 };
365 auto changeIndexToIndex = [this]() {
366 (static_cast<DataModel<T>*>(tableModel.get()))->setDataSource(new TensorSliceDataSource(shape, dimVertical, dimHorizontal, mkIdxs(), data));
367 };
368 switch (index) {
369 case 0: // change to X
370 if (dim==dimHorizontal)
371 return; // same
372 else if (dim==dimVertical) // Y->X
373 swapXY(dimVertical, dimHorizontal, true/*iAmY*/);
374 else { // other index -> X
375 changeIndexToXY(dimHorizontal);
376 }
377 break;
378 case 1: // change to Y
379 if (dim==dimVertical)
380 return; // same
381 else if (dim==dimHorizontal) // X->Y
382 swapXY(dimHorizontal, dimVertical, false/*iAmY*/);
383 else { // other index -> Y
384 changeIndexToXY(dimVertical);
385 }
386 break;
387 default: // change to index
388 if (dim==dimVertical) // Y->index
389 changeXYtoIndex(dimVertical, true/*iAmY*/);
390 else if (dim==dimHorizontal) // X->index
391 changeXYtoIndex(dimHorizontal, false/*iAmY*/);
392 else // index->index
393 changeIndexToIndex();
394 }
395 });
396 shapeDimensionsComboBoxes.push_back(std::unique_ptr<QComboBox>(combobox));
397 combobox->setToolTip(QString(tr("Directions and indexes to choose for dimension number %1 of the shape %2")).arg(dim+1).arg(S2Q(STR(shape))));
398 dim++;
399 }
400
401 // choose the initial vertical and horizontal dimensions
402 switch (numMultiDims) {
403 case 0:
404 case 1:
405 assert(false); // 0 is invalid, 1 should be handled by the 1D class
406 default:
407 // in case of 2 - choose the only two that are avalable, in other cases choose two before the very last one, because the last one is usually a channel dimension
408 unsigned num = 0, numMatch = (numMultiDims==2 ? 0 : numMultiDims-3), idx = 0;
409 for (auto d : shape) {
410 if (d > 1) {
411 if (num == numMatch)
412 dimVertical = idx;
413 else if (num == numMatch+1) {
414 dimHorizontal = idx;
415 break;
416 }
417 num++;
418 }
419 idx++;
420 }
421 break;
422 }
423 for (unsigned dim = 0; dim < shapeDimensionsComboBoxes.size(); dim++) {
424 if (dim == dimVertical)
425 shapeDimensionsComboBoxes[dim]->setCurrentIndex(1/*Y*/);
426 else if (dim == dimHorizontal)
427 shapeDimensionsComboBoxes[dim]->setCurrentIndex(0/*X*/);
428 else
429 shapeDimensionsComboBoxes[dim]->setCurrentIndex(2/*index=1*/);
430 }
431 }
432
433 // alignment
434 colorSchemaLabel.setAlignment(Qt::AlignRight|Qt::AlignVCenter);
435
436 // size policies
437 headerWidget .setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
438 colorSchemaComboBox .setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed); // (H) The sizeHint() is a maximum
439 header1Widget .setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
440 filler1Widget .setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Maximum); // (H) The widget can be expanded (V) The sizeHint() is a maximum
441 scaleBwImageLabel .setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
442 scaleBwImageSpinBox .setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
443 viewDataAsBwImageCheckBox.setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed); // (H) The sizeHint() is a maximum
444 tableView .setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
445 //imageView .setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
446 for (auto widget : {&shapeLabel, &dataRangeLabel, &colorSchemaLabel}) // make labels minimal
447 widget->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
448 layout.setSpacing(0);
449 layout.setContentsMargins(0,0,0,0);
450 headerWidget.setContentsMargins(0,0,0,0);
451 headerLayout.setContentsMargins(0,0,0,0); // XXX this doesn't work for some reason: widget still has vertical margins
452 header1Layout.setContentsMargins(0,0,0,0);
453
454 // data range
455 auto dataRange = Util::arrayMinMax(data, Tensor::flatSize(shape));
456 dataRangeLabel.setText(QString(tr("Data Range: %1..%2")).arg(std::get<0>(dataRange)).arg(std::get<1>(dataRange)));
457
458 // create the model
459 tableModel.reset(new DataModel<T>(new TensorSliceDataSource(shape, dimVertical, dimHorizontal, mkIdxs(), data), nullptr, &tableView));
460 tableView.setModel(tableModel.get());
461
462 // combobox values
463 colorSchemaComboBox.addItem(tr("None (default)"), COLORSCHEME_NONE);
464 colorSchemaComboBox.addItem(tr("Blue-to-Red"), COLORSCHEME_BLUE_TO_RED);
465 colorSchemaComboBox.addItem(tr("Red-to-Blue"), COLORSCHEME_RED_TO_BLUE);
466 colorSchemaComboBox.addItem(tr("Grayscale Up"), COLORSCHEME_GRAYSCALE_UP);
467 colorSchemaComboBox.addItem(tr("Grayscale Down"), COLORSCHEME_GRAYSCALE_DOWN);
468 colorSchemaComboBox.addItem(tr("Grayscale Zero Up"), COLORSCHEME_GRAYSCALE_ZERO_UP);
469 colorSchemaComboBox.addItem(tr("Grayscale Zero Down"), COLORSCHEME_GRAYSCALE_ZERO_DOWN);
470 colorSchemaComboBox.addItem(tr("Grayscale Middle Up"), COLORSCHEME_GRAYSCALE_MIDDLE_UP);
471 colorSchemaComboBox.addItem(tr("Grayscale Middle Down"), COLORSCHEME_GRAYSCALE_MIDDLE_DOWN);
472
473 // set up the spin-box
474 scaleBwImageSpinBox.setSuffix(tr(" times"));
475 //scaleBwImageSpinBox.lineEdit()->setReadOnly(true);
476
477 // visibility
478 scaleBwImageLabel.setVisible(false); // scale widgets are only visible when the image to scale is displayed
479 scaleBwImageSpinBox.setVisible(false);
480
481 // values
482 viewDataAsBwImageCheckBox.setChecked(appSettings.value("DataTable2D.viewAsImages", true).toBool()); // view mode based on user's choice
483
484 // set the initial mode
485 setShowImageViewMode(viewDataAsBwImageCheckBox.isChecked());
486
487 // connect signals
488 connect(&colorSchemaComboBox, QOverload<int>::of(&QComboBox::activated), [this,dataRange](int index) {
489 (static_cast<DataModel<T>*>(tableModel.get()))->setColorSchema(createColorSchema(
490 (ColorSchemaEnum)colorSchemaComboBox.itemData(index).toInt(),
491 std::get<0>(dataRange),
492 std::get<1>(dataRange)
493 ));
494 });
495 connect(&scaleBwImageSpinBox, QOverload<int>::of(&QSpinBox::valueChanged), [this](int i) {
496 if (self)
497 return;
498 updateBwImageView(false/*initialUpdate*/);
499 });
500 connect(&viewDataAsBwImageCheckBox, &QCheckBox::stateChanged, [this](int state) {
501 bool showImageView = state!=0;
502 // save user's choice
503 appSettings.setValue("DataTable2D.viewAsImages", showImageView);
504 // set the mode
505 setShowImageViewMode(showImageView);
506
507 });
508
509 // tooltips
510 shapeLabel .setToolTip(tr("Shape of tensor data that this table represents"));
511 dataRangeLabel .setToolTip(tr("Range of numeric values present in the table"));
512 colorSchemaComboBox .setToolTip(tr("Change the color schema of data visualization"));
513 viewDataAsBwImageCheckBox.setToolTip(tr("View data as image"));
514 tableView .setToolTip(tr("Tensor data values"));
515 }
516
517 /// interface
518
519 template<typename T>
dataChanged(const void * data)520 void DataTable2D<T>::dataChanged(const void *data) {
521 dataChangedTyped(static_cast<const T*>(data));
522 }
523
524 /// internals
525
526 template<typename T>
dataChangedTyped(const T * data_)527 void DataTable2D<T>::dataChangedTyped(const T *data_) {
528 // update the numeric table
529 (static_cast<DataModel<T>*>(tableModel.get()))->beginResetModel();
530 data = data_;
531 (static_cast<DataModel<T>*>(tableModel.get()))->endResetModel();
532 // update XRay-style view if it is enabled
533 if (viewDataAsBwImageCheckBox.isChecked())
534 updateBwImageView(false/*initialUpdate*/);
535 }
536
537 template<typename T>
mkIdxs() const538 std::vector<unsigned> DataTable2D<T>::mkIdxs() const {
539 std::vector<unsigned> idxs;
540 unsigned i = 0;
541 for (auto d : shape) {
542 if (i==dimVertical || i==dimHorizontal || d==1)
543 idxs.push_back(0);
544 else
545 idxs.push_back(shapeDimensionsComboBoxes[i]->currentIndex()-2);
546 i++;
547 }
548 return idxs;
549 }
550
551 template<typename T>
updateBwImageView(bool initialUpdate)552 void DataTable2D<T>::updateBwImageView(bool initialUpdate) {
553 // helpers
554 auto dataSourceToBwImage = [](const DataSource<T> *dataSource, unsigned scaleFactor, std::tuple<T,T> &minMax) {
555 std::unique_ptr<uchar> data(new uchar[dataSource->ncols()*dataSource->nrows()*scaleFactor*scaleFactor]);
556 minMax = dataSource->computeMinMax();
557 auto minMaxRange = std::get<1>(minMax)-std::get<0>(minMax);
558 auto normalize = [minMax,minMaxRange](T d) {
559 return 255.*(d-std::get<0>(minMax))/minMaxRange;
560 };
561 auto *p = data.get();
562 for (unsigned r = 0, re = dataSource->nrows(); r < re; r++)
563 for (unsigned rptRow = 0; rptRow<scaleFactor; rptRow++)
564 for (unsigned c = 0, ce = dataSource->ncols(); c < ce; c++)
565 for (unsigned rptCol = 0; rptCol<scaleFactor; rptCol++)
566 *p++ = normalize(dataSource->value(r,c));
567 auto dataPtr = data.release();
568 return std::tuple<std::unique_ptr<uchar>,QImage>(
569 std::unique_ptr<uchar>(dataPtr),
570 QImage( dataPtr
571 , dataSource->ncols()*scaleFactor // width
572 , dataSource->nrows()*scaleFactor // height
573 , dataSource->ncols()*scaleFactor*sizeof(uchar) // bytesPerLine
574 , QImage::Format_Grayscale8
575 )
576 );
577 };
578
579 // list all combinations
580 std::vector<std::vector<unsigned>> indexes; // elements are sized like the shape, with ones in places of X and Y // indexes are 0-based
581 {
582 std::function<void(unsigned pos, std::vector<unsigned> curr, const std::vector<unsigned> &dims, std::vector<std::vector<unsigned>> &indexes)> iterate;
583 iterate = [&iterate](unsigned pos, std::vector<unsigned> curr, const std::vector<unsigned> &dims, std::vector<std::vector<unsigned>> &indexes) {
584 curr.push_back(0);
585 for (auto &index = *curr.rbegin(); index < dims[pos]; index++)
586 if (pos+1 < dims.size())
587 iterate(pos+1, curr, dims, indexes);
588 else
589 indexes.push_back(curr);
590 curr.resize(curr.size()-1);
591 };
592
593 auto dims = shape;
594 dims[dimVertical] = 1; // X and Y are set to 1 - we don't need to list them because they are on X,Y axes of pictures
595 dims[dimHorizontal] = 1;
596 iterate(0, {}, dims, indexes);
597 }
598
599 auto fmtIndex = [this](const std::vector<unsigned> &index) {
600 std::ostringstream ss;
601 ss << "[";
602 unsigned n = 0;
603 for (auto i : index) {
604 ss << (n==0 ? "" : ",") << (n==dimVertical ? "Y" : n==dimHorizontal ? "X" : STR(i+1));
605 ++n;
606 }
607 ss << "]";
608 return ss.str();
609 };
610
611
612 if (initialUpdate) { // decide on a scaling factor
613 auto maxLabelWidth = QFontMetrics(shapeLabel.font()).size(0/*flags*/,
614 QString("%1\n%2 .. %3").arg(S2Q(fmtIndex({1000,1000,1000,1000}))).arg(-1.234567891).arg(+1.234567891)).width();
615 unsigned minScaleFactor = maxLabelWidth/shape[dimHorizontal];
616 if (minScaleFactor == 0)
617 minScaleFactor = 1;
618 else
619 while (minScaleFactor*shape[dimHorizontal] < maxLabelWidth)
620 minScaleFactor++;
621 self = true;
622 scaleBwImageSpinBox.setRange(minScaleFactor, 5*minScaleFactor); // XXX sizing issues and image not being in the center issues arise when
623 scaleBwImageSpinBox.setValue(minScaleFactor); // images are allowed to be smaller tha labels
624 self = false;
625 }
626
627 const unsigned numColumns = 16; // TODO should be based on width()/cell.width // TODO scaling coefficient initial value should also be adaptable
628 const unsigned numRows = (indexes.size()+numColumns-1)/numColumns;
629 imageView.setSizesAndData(numColumns/*width*/, numRows/*height*/, numColumns - (numRows*numColumns - indexes.size()), [&](unsigned x, unsigned y) {
630 std::unique_ptr<DataSource<T>> dataSource(new TensorSliceDataSource<T>(shape, dimVertical, dimHorizontal, indexes[y*numColumns+x], data));
631 std::tuple<T,T> minMax;
632 auto imageWithData = dataSourceToBwImage(dataSource.get(), scaleBwImageSpinBox.value()/*scale 1+*/, minMax);
633 return ImageGridWidget::ImageData(
634 QString("%1\n%2 .. %3")
635 .arg(S2Q(fmtIndex(indexes[y*numColumns+x])))
636 .arg(std::get<0>(minMax))
637 .arg(std::get<1>(minMax)),
638 std::unique_ptr<uchar>(std::get<0>(imageWithData).release()),
639 std::get<1>(imageWithData)
640 );
641 });
642 }
643
644 template<typename T>
setShowImageViewMode(bool showImageView)645 void DataTable2D<T>::setShowImageViewMode(bool showImageView) {
646 // switch the view
647 dataViewStackWidget.setCurrentIndex(showImageView ? 1/*imageView*/ : 0/*tableView*/);
648 // update the screen
649 if (showImageView) {
650 if (!imageViewInitialized) {
651 // create the image from the datasource that the table sees
652 updateBwImageView(true/*initialUpdate*/);
653 // set tooltip with explanation custom to the data
654 imageView.setToolTip(QString(tr("Tensor data as a B/W image normalized to the data range of currently viewed tensor slice")));
655 imageViewInitialized = true;
656 }
657 // disable all other widgets so that the data view can't be changed
658 headerWidget.setEnabled(false);
659 } else {
660 headerWidget.setEnabled(true);
661 }
662 // visibility of scale controls
663 scaleBwImageLabel.setVisible(showImageView);
664 scaleBwImageSpinBox.setVisible(showImageView);
665 }
666
667 // template instantiation
668 template class DataTable2D<half_float::half>; // for float16
669 template class DataTable2D<float>; // for float32
670 template class DataTable2D<double>; // for float64
671 template class DataTable2D<int8_t>; // for int8
672 template class DataTable2D<uint8_t>; // for uint8
673 template class DataTable2D<int16_t>; // for int16
674 template class DataTable2D<int32_t>; // for int32
675 template class DataTable2D<int64_t>; // for int64
676