1 package nom.tam.image.compression;
2
3 import java.io.IOException;
4 import java.nio.Buffer;
5 import java.nio.ByteBuffer;
6 import java.util.ArrayList;
7 import java.util.Arrays;
8 import java.util.List;
9 import java.util.logging.Logger;
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 import nom.tam.fits.FitsException;
43 import nom.tam.fits.Header;
44 import nom.tam.fits.compression.algorithm.api.ICompressOption;
45 import nom.tam.fits.compression.algorithm.api.ICompressorControl;
46 import nom.tam.fits.compression.algorithm.quant.QuantizeOption;
47 import nom.tam.fits.compression.algorithm.rice.RiceCompressOption;
48 import nom.tam.fits.compression.provider.CompressorProvider;
49 import nom.tam.fits.header.Compression;
50 import nom.tam.fits.header.Standard;
51 import nom.tam.image.ImageTiler;
52 import nom.tam.image.StandardImageTiler;
53 import nom.tam.image.compression.hdu.CompressedImageHDU;
54 import nom.tam.util.ArrayDataOutput;
55 import nom.tam.util.ArrayFuncs;
56 import nom.tam.util.type.ElementType;
57
58
59
60
61
62
63
64 public class CompressedImageTiler implements ImageTiler {
65 private static final Logger LOGGER = Logger.getLogger(CompressedImageTiler.class.getName());
66
67 static final int DEFAULT_BLOCK_SIZE = 32;
68
69
70
71
72
73
74
75
76
77
78
79
80 static boolean incrementPosition(int[] start, int[] current, int[] lengths, int[] steps) {
81 for (int i = start.length - 2; i >= 0; i--) {
82 if (current[i] - start[i] < lengths[i] - steps[i]) {
83 current[i] += steps[i];
84 if (start.length - 1 - (i + 1) >= 0) {
85 System.arraycopy(start, i + 1, current, i + 1, start.length - 1 - (i + 1));
86 }
87 return true;
88 }
89 }
90 return false;
91 }
92
93
94
95
96
97
98
99
100
101
102 static boolean isValidSegment(final int position, final int length, final int dimension) {
103 return position + length >= 0 && position < dimension;
104 }
105
106 private final CompressedImageHDU compressedImageHDU;
107
108 private final List<String> columnNames = new ArrayList<>();
109
110
111
112
113
114
115 public CompressedImageTiler(final CompressedImageHDU compressedImageHDU) {
116 this.compressedImageHDU = compressedImageHDU;
117 init();
118 }
119
120 void init() {
121 final int columnCount = compressedImageHDU.getData().getNCols();
122 final Header header = compressedImageHDU.getHeader();
123
124 for (int index = 0; index < columnCount; index++) {
125 final String ttype = header.getStringValue(Standard.TTYPEn.n(index + 1));
126 if (ttype != null) {
127 addColumn(ttype.trim());
128 }
129 }
130 }
131
132 void addColumn(final String column) {
133 columnNames.add(column);
134 }
135
136
137
138
139
140
141
142
143
144
145
146
147
148 void getTile(final ArrayDataOutput output, final int[] imageDimensions, final int[] corners, final int[] lengths,
149 final int[] steps) throws IOException, FitsException {
150
151 final int n = imageDimensions.length;
152 final int[] posits = new int[n];
153
154
155 final int segmentStepValue = steps[n - 1];
156
157 final int segment = lengths[n - 1];
158
159
160 final Class<?> base = getBaseType().primitiveClass();
161
162 System.arraycopy(corners, 0, posits, 0, n);
163 final int[] tileDimensions = ArrayFuncs.reverseIndices(getTileDimensions());
164
165 do {
166
167
168
169 final int mx = imageDimensions.length - 1;
170 boolean validSegment = CompressedImageTiler.isValidSegment(posits[mx], lengths[mx], imageDimensions[mx]);
171
172 if (validSegment) {
173 int stepOffset = 0;
174 int pixelsRead = 0;
175 final int[] tileRowPositions = new int[n];
176 System.arraycopy(posits, 0, tileRowPositions, 0, n);
177 while (pixelsRead < segment) {
178 final int[] tileOffsets = getTileOffsets(tileRowPositions, tileDimensions);
179
180 final Object tileData = getDecompressedTileData(tileRowPositions, tileDimensions);
181 final StandardImageTiler standardImageTiler = new StandardImageTiler(null, -1, tileDimensions, base) {
182 @Override
183 protected Object getMemoryImage() {
184 return tileData;
185 }
186 };
187 final int remaining = segment - pixelsRead;
188
189
190 tileOffsets[mx] += stepOffset;
191 final int segmentLength = Math.min(segment, tileDimensions[mx] - tileOffsets[mx]);
192 final int tileReadLength = Math.max(1, Math.min(remaining, segmentLength));
193
194 final int[] tileReadLengths = new int[tileDimensions.length];
195 Arrays.fill(tileReadLengths, 1);
196 tileReadLengths[tileReadLengths.length - 1] = tileReadLength;
197
198 final int[] tileSteps = new int[tileDimensions.length];
199 Arrays.fill(tileSteps, 1);
200 tileSteps[tileSteps.length - 1] = segmentStepValue;
201
202
203 standardImageTiler.getTile(output, tileOffsets, tileReadLengths, tileSteps);
204 final int unreadSteps = tileReadLength % segmentStepValue;
205
206 stepOffset = (unreadSteps > 0) ? segmentStepValue - unreadSteps : 0;
207 pixelsRead += tileReadLength;
208 tileRowPositions[mx] = tileRowPositions[mx] + tileReadLength;
209 }
210 }
211 } while (CompressedImageTiler.incrementPosition(corners, posits, lengths, steps));
212 output.flush();
213 }
214
215
216
217
218
219
220
221
222
223
224
225 Object getDecompressedTileData(final int[] positions, final int[] tileDimensions) throws FitsException {
226 final int compressedDataColumnIndex = columnNames.indexOf(Compression.COMPRESSED_DATA_COLUMN);
227 final int uncompressedDataColumnIndex = columnNames.indexOf(Compression.UNCOMPRESSED_DATA_COLUMN);
228 final int gZipCompressedDataColumnIndex = columnNames.indexOf(Compression.GZIP_COMPRESSED_DATA_COLUMN);
229 final Object[] row = getRow(positions, tileDimensions);
230 final Object decompressedArray;
231
232 final byte[] compressedRowData = (byte[]) row[compressedDataColumnIndex];
233 if (compressedRowData.length > 0) {
234 decompressedArray = decompressRow(compressedDataColumnIndex, row);
235 } else if (gZipCompressedDataColumnIndex >= 0) {
236 decompressedArray = decompressRow(gZipCompressedDataColumnIndex, row);
237 } else if (uncompressedDataColumnIndex >= 0) {
238 decompressedArray = row[uncompressedDataColumnIndex];
239 } else {
240 throw new FitsException("Nothing in row to read: (" + Arrays.deepToString(row) + ").");
241 }
242
243 return ArrayFuncs.curl(decompressedArray, tileDimensions);
244 }
245
246 int[] getTileIndexes(final int[] pixelPositions, final int[] tileDimensions) {
247 final int[] tileIndexes = new int[pixelPositions.length];
248
249 for (int i = 0; i < pixelPositions.length; i++) {
250 tileIndexes[i] = pixelPositions[i] / tileDimensions[i];
251 }
252
253 return tileIndexes;
254 }
255
256
257
258
259
260
261
262
263
264
265
266 Object decompressRow(final int columnIndex, final Object[] row) throws FitsException {
267 final byte[] compressedRowData = (byte[]) row[columnIndex];
268
269
270 final ByteBuffer compressed = ByteBuffer.wrap(compressedRowData);
271 compressed.rewind();
272
273 try {
274 final Buffer tileBuffer = decompressIntoBuffer(row, compressed);
275 if (hasData(tileBuffer)) {
276 return tileBuffer.array();
277 }
278 throw new FitsException("No tile available at column " + columnIndex + ": (" + Arrays.deepToString(row) + ")");
279 } catch (IllegalStateException illegalStateException) {
280
281
282 LOGGER.severe(
283 "Unable to decompress row data from column " + columnIndex + ": (" + Arrays.deepToString(row) + ")");
284 throw new FitsException(illegalStateException.getMessage(), illegalStateException);
285 }
286 }
287
288
289
290
291
292
293
294
295
296 Buffer decompressIntoBuffer(final Object[] row, final ByteBuffer compressed) {
297 final ElementType<Buffer> bufferElementType = getBaseType();
298 final Buffer tileBuffer = bufferElementType.newBuffer(getTileSize());
299 tileBuffer.rewind();
300 final ICompressorControl compressorControl = getCompressorControl(getBaseType());
301 final ICompressOption option = initCompressionOption(compressorControl.option(), bufferElementType.size());
302 initRowOption(option, row);
303 compressorControl.decompress(compressed, tileBuffer, option);
304
305 tileBuffer.rewind();
306 return tileBuffer;
307 }
308
309 ICompressorControl getCompressorControl(final ElementType<? extends Buffer> elementType) {
310 return CompressorProvider.findCompressorControl(getQuantizAlgorithmName(), getCompressionAlgorithmName(),
311 elementType.primitiveClass());
312 }
313
314 ICompressOption initCompressionOption(final ICompressOption option, final int bytePix) {
315 if (option instanceof RiceCompressOption) {
316 ((RiceCompressOption) option).setBlockSize(getBlockSize());
317 ((RiceCompressOption) option).setBytePix(bytePix);
318 } else if (option instanceof QuantizeOption) {
319 initCompressionOption(((QuantizeOption) option).getCompressOption(), bytePix);
320 }
321
322 option.setTileHeight(getTileHeight()).setTileWidth(getTileWidth());
323
324 return option;
325 }
326
327 ElementType<Buffer> getBaseType() {
328 final int zBitPix = getZBitPix();
329 final ElementType<Buffer> bufferElementType = ElementType.forBitpix(zBitPix);
330 if (bufferElementType == null) {
331 return ElementType.forNearestBitpix(zBitPix);
332 }
333 return bufferElementType;
334 }
335
336
337
338
339
340
341
342
343 boolean hasData(final Buffer buffer) {
344 return buffer.hasArray();
345 }
346
347
348
349
350
351
352
353
354
355
356
357 Object[] getRow(final int[] positions, final int[] tileDimensions) throws FitsException {
358 final int[] tileIndexes = getTileIndexes(ArrayFuncs.reverseIndices(positions),
359 ArrayFuncs.reverseIndices(tileDimensions));
360 final int rowNumber = getRowNumber(tileIndexes);
361 return compressedImageHDU.getRow(rowNumber);
362 }
363
364 int getRowNumber(final int[] tileIndexes) throws FitsException {
365 int offset = 0;
366 final int[] tableDimensions = getTableDimensions();
367 for (int i = 0; i < tableDimensions.length; i++) {
368 if (i > 0) {
369 offset += tileIndexes[i] * tableDimensions[i - 1];
370 } else {
371 offset += tileIndexes[i];
372 }
373 }
374 return offset;
375 }
376
377
378
379
380
381
382
383
384 int[] getTileOffsets(final int[] corners, final int[] tileDimensions) {
385 final int numberOfDimensions = getNumberOfDimensions();
386 final int[] tileOffsets = new int[numberOfDimensions];
387
388 for (int i = 0; i < numberOfDimensions; i++) {
389 final int tileDimension = tileDimensions[i];
390 final int pixelOffset = corners[i];
391 if (pixelOffset % tileDimension == 0 || tileDimension == 1) {
392 tileOffsets[i] = 0;
393 } else {
394 final int currentTile = corners[i] / tileDimensions[i];
395 final int lastTile = Math.max(0, currentTile - 1);
396
397
398 final int pixelsToEndOfLastTile = (lastTile * tileDimension) + tileDimension;
399 final int tileOffset;
400 if (pixelOffset < tileDimension) {
401 tileOffset = pixelOffset;
402 } else {
403 tileOffset = pixelOffset - pixelsToEndOfLastTile;
404 }
405
406 tileOffsets[i] = tileOffset;
407 }
408 }
409
410 return tileOffsets;
411 }
412
413 @Override
414 public Object getCompleteImage() throws IOException {
415 try {
416 return compressedImageHDU.asImageHDU().getData().getData();
417 } catch (FitsException fitsException) {
418 throw new IOException(fitsException.getMessage(), fitsException);
419 }
420 }
421
422 @Override
423 public void getTile(Object output, int[] corners, int[] lengths) throws IOException {
424 final int[] steps = new int[lengths.length];
425 Arrays.fill(steps, 1);
426 getTile(output, corners, lengths, steps);
427 }
428
429 @Override
430 public void getTile(Object output, int[] corners, int[] lengths, int[] steps) throws IOException {
431 final int[] imageDimensions = getImageDimensions();
432
433 if (corners.length != imageDimensions.length || lengths.length != imageDimensions.length
434 || steps.length != imageDimensions.length) {
435 throw new IOException("Inconsistent sub-image request");
436 }
437 if (output == null) {
438 throw new IOException("Attempt to write to null data output");
439 }
440 for (int i = 0; i < imageDimensions.length; i++) {
441 if (corners[i] < 0 || lengths[i] < 0 || corners[i] + lengths[i] > imageDimensions[i]) {
442 throw new IOException("Sub-image not within image");
443 }
444 }
445
446 if (!(output instanceof ArrayDataOutput)) {
447 throw new UnsupportedOperationException("Only streaming to ArrayDataOutput is supported. "
448 + "See getTile(ArrayDataOutput, int[], int[], int[].");
449 }
450 try {
451 getTile((ArrayDataOutput) output, imageDimensions, corners, lengths, steps);
452 } catch (FitsException fitsException) {
453 throw new IOException(fitsException.getMessage(), fitsException);
454 }
455 }
456
457 @Override
458 public Object getTile(int[] corners, int[] lengths) throws IOException {
459 throw new UnsupportedOperationException(
460 "Only streaming to ArrayDataOutput is supported. " + "See getTile(ArrayDataOutput, int[], int[], int[].");
461 }
462
463 void initRowOption(final ICompressOption option, final Object[] row) {
464 final int zScaleColumnIndex = columnNames.indexOf(Compression.ZSCALE_COLUMN);
465 final int zZeroColumnIndex = columnNames.indexOf(Compression.ZZERO_COLUMN);
466 if (option instanceof QuantizeOption) {
467 final double bScale = zScaleColumnIndex >= 0 ? ((double[]) row[zScaleColumnIndex])[0] : Double.NaN;
468 ((QuantizeOption) option).setBScale(bScale);
469
470 final double bZero = zZeroColumnIndex >= 0 ? ((double[]) row[zZeroColumnIndex])[0] : Double.NaN;
471 ((QuantizeOption) option).setBZero(bZero);
472 }
473 }
474
475 Header getHeader() {
476 return compressedImageHDU.getHeader();
477 }
478
479 private String getQuantizAlgorithmName() {
480 return getHeader().getStringValue(Compression.ZQUANTIZ);
481 }
482
483 private String getCompressionAlgorithmName() {
484 return getHeader().getStringValue(Compression.ZCMPTYPE);
485 }
486
487 int getBlockSize() {
488 final int axesLength = getNumberOfDimensions();
489 for (int i = 0; i < axesLength; i++) {
490 final int nextAxis = i + 1;
491 final String zNameValue = getHeader().getStringValue(Compression.ZNAMEn.n(nextAxis));
492 if (Compression.BLOCKSIZE.equals(zNameValue)) {
493 return getHeader().getIntValue(Compression.ZVALn.n(nextAxis));
494 }
495 }
496
497 return DEFAULT_BLOCK_SIZE;
498 }
499
500
501
502
503
504
505 int getNumberOfDimensions() {
506 return getHeader().getIntValue(Compression.ZNAXIS);
507 }
508
509 int getZBitPix() {
510 return getHeader().getIntValue(Compression.ZBITPIX);
511 }
512
513 int getImageAxisLength(final int axis) {
514 return getHeader().getIntValue(Compression.ZNAXISn.n(axis));
515 }
516
517 int[] getTableDimensions() throws FitsException {
518 final int n = getNumberOfDimensions();
519 final int[] tableDimensions = new int[n];
520 for (int i = 0; i < n; i++) {
521 tableDimensions[i] = Double
522 .valueOf(Math
523 .ceil(Integer.valueOf(getImageAxisLength(i + 1)).doubleValue() / getTileDimensionLength(i + 1)))
524 .intValue();
525 }
526
527 return tableDimensions;
528 }
529
530
531
532
533
534
535 int[] getImageDimensions() {
536 final int n = getNumberOfDimensions();
537 final int[] imageDimensions = new int[n];
538 final Header header = getHeader();
539 for (int i = 0; i < n; i++) {
540 imageDimensions[n - i - 1] = header.getIntValue(Compression.ZNAXISn.n(i + 1));
541 }
542
543 return imageDimensions;
544 }
545
546 int getTileHeight() {
547 return getHeader().getIntValue(Compression.ZTILEn.n(2), 1);
548 }
549
550 int getTileWidth() {
551 return getHeader().getIntValue(Compression.ZTILEn.n(1), getHeader().getIntValue(Compression.ZNAXISn.n(1)));
552 }
553
554 int[] getTileDimensions() throws FitsException {
555 final int totalDimensions = getNumberOfDimensions();
556 final int[] tileDimensions = new int[totalDimensions];
557 for (int n = 0; n < totalDimensions; n++) {
558 tileDimensions[n] = getTileDimensionLength(n + 1);
559 }
560
561 return tileDimensions;
562 }
563
564 int getTileDimensionLength(final int dimension) throws FitsException {
565 final int totalDimensions = getNumberOfDimensions();
566
567 if (dimension < 1) {
568 throw new FitsException("Dimensions are 1-based (got " + dimension + ").");
569 }
570 if (dimension > totalDimensions) {
571 throw new FitsException("Trying to get tile for dimension " + dimension + " where there are only "
572 + totalDimensions + " dimensions in total.");
573 }
574
575 final int dimensionLength;
576 if (dimension == 1) {
577 dimensionLength = getHeader().getIntValue(Compression.ZTILEn.n(1),
578 getHeader().getIntValue(Compression.ZNAXISn.n(1)));
579 } else {
580 dimensionLength = getHeader().getIntValue(Compression.ZTILEn.n(dimension), 1);
581 }
582
583 return dimensionLength;
584 }
585
586 int getTileSize() {
587 final int n = getNumberOfDimensions();
588 int tileSize = getHeader().getIntValue(Compression.ZTILEn.n(1), getHeader().getIntValue(Compression.ZNAXISn.n(1)));
589 for (int i = 2; i <= n; i++) {
590 tileSize *= getHeader().getIntValue(Compression.ZTILEn.n(i), 1);
591 }
592
593 return tileSize;
594 }
595 }