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