From 35a1e70547a08b05555995aaf047df2307c85914 Mon Sep 17 00:00:00 2001 From: eddyem Date: Mon, 22 Sep 2014 14:48:19 +0400 Subject: [PATCH] ported to git --- .gitignore | 3 + CMakeLists.txt | 24 + COPYING | 2 + ChangeLog | 99 ++ INSTALL | 0 NEWS | 1 + README | 6 + src/CMakeLists.txt | 88 ++ src/CUDA.cu | 693 +++++++++++++ src/NOCUDA.c | 816 +++++++++++++++ src/contours.c | 506 +++++++++ src/filelist.c | 168 +++ src/fits.c | 396 +++++++ src/fitsheaders.c | 530 ++++++++++ src/fitsview | Bin 0 -> 264609 bytes src/fitsview.c | 123 +++ src/fitsview.glade | 1358 +++++++++++++++++++++++++ src/fitsview.ui | 1281 +++++++++++++++++++++++ src/gauss.c | 378 +++++++ src/gtk.c | 1204 ++++++++++++++++++++++ src/imtools.c | 638 ++++++++++++ src/include/CUtools.h | 51 + src/include/contours.h | 9 + src/include/filelist.h | 19 + src/include/fits.h | 11 + src/include/fitsheaders.h | 20 + src/include/fitsview.h | 233 +++++ src/include/gauss.h | 20 + src/include/gtk.h | 92 ++ src/include/imtools.h | 19 + src/include/open_dialog.h | 8 + src/include/opengl.h | 21 + src/include/spots.h | 61 ++ src/include/terrain.h | 9 + src/include/tracking.h | 31 + src/locale/ru/LC_MESSAGES/fitsview.mo | Bin 0 -> 8870 bytes src/locale/ru/messages.po | 596 +++++++++++ src/locale/ru/ru.po | 590 +++++++++++ src/locale/ru/update | 2 + src/open_dialog.c | 433 ++++++++ src/opengl.c | 731 +++++++++++++ src/scripts/genh | 3 + src/spots.c | 534 ++++++++++ src/terrain.c | 423 ++++++++ src/tracking.c | 565 ++++++++++ src/ui.h | 1281 +++++++++++++++++++++++ 46 files changed, 14076 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 COPYING create mode 100644 ChangeLog create mode 100644 INSTALL create mode 100644 NEWS create mode 100644 README create mode 100644 src/CMakeLists.txt create mode 100644 src/CUDA.cu create mode 100644 src/NOCUDA.c create mode 100644 src/contours.c create mode 100644 src/filelist.c create mode 100644 src/fits.c create mode 100644 src/fitsheaders.c create mode 100755 src/fitsview create mode 100644 src/fitsview.c create mode 100644 src/fitsview.glade create mode 100644 src/fitsview.ui create mode 100644 src/gauss.c create mode 100644 src/gtk.c create mode 100644 src/imtools.c create mode 100644 src/include/CUtools.h create mode 100644 src/include/contours.h create mode 100644 src/include/filelist.h create mode 100644 src/include/fits.h create mode 100644 src/include/fitsheaders.h create mode 100644 src/include/fitsview.h create mode 100644 src/include/gauss.h create mode 100644 src/include/gtk.h create mode 100644 src/include/imtools.h create mode 100644 src/include/open_dialog.h create mode 100644 src/include/opengl.h create mode 100644 src/include/spots.h create mode 100644 src/include/terrain.h create mode 100644 src/include/tracking.h create mode 100644 src/locale/ru/LC_MESSAGES/fitsview.mo create mode 100644 src/locale/ru/messages.po create mode 100644 src/locale/ru/ru.po create mode 100755 src/locale/ru/update create mode 100644 src/open_dialog.c create mode 100644 src/opengl.c create mode 100755 src/scripts/genh create mode 100644 src/spots.c create mode 100644 src/terrain.c create mode 100644 src/tracking.c create mode 100644 src/ui.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2ea1a4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.hg* +*~ +*.bak diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d25dc3f --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 2.6) +set(PROJ fitsview) +project(${PROJ}) +set(CMAKE_COLOR_MAKEFILE ON) +set(DEBUG 1) # закомментировать эту строку по окончании "разработки" +if(NOT DEFINED NO_CUDA) + message("Try to use CUDA") + find_package(CUDA) + if(CUDA_FOUND) + add_definitions(-DCUDA_FOUND) + endif() +endif() +if(NOT DEFINED PROCESSOR_COUNT) + set(PROCESSOR_COUNT 2) # by default 2 cores + set(cpuinfo_file "/proc/cpuinfo") + if(EXISTS "${cpuinfo_file}") + file(STRINGS "${cpuinfo_file}" procs REGEX "^processor.: [0-9]+$") + list(LENGTH procs PROCESSOR_COUNT) + endif() +endif() +add_definitions(-DTHREAD_NUMBER=${PROCESSOR_COUNT}) +message("In multithreaded operations will use ${PROCESSOR_COUNT} threads") +subdirs(src) + diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..256a913 --- /dev/null +++ b/COPYING @@ -0,0 +1,2 @@ +eddy@sao.ru + diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..fc658ec --- /dev/null +++ b/ChangeLog @@ -0,0 +1,99 @@ +01.12.2011 Edward V. Emelianoff + + * Изменена функция определения изолиний. Теперь медлненно, но надежно. Планирую ускорить + - обнаружены ошибки при открывании файла сразу в 3D + - неверно отображаются изолинии на 3D при выборе части изображения + + +06.09.2011 Edward V. Emelianoff + + * добавлен поиск изолиний (с различными типами шкалы высот) + - АЛГОРИТМ ЖУТКО ГЛЮЧИТ + - сегфолт на экспоненциальной шкале высот + - почему-то некоторые контуры получаются разорванными, контуры выходят за пределы рисунка + + +29.08.2011 Edward V. Emelianoff + + * добавлен медианный фильтр + + +22.08.2011 Edward V. Emelianoff + + * убран сегфолт при попытке открыть битую симв. ссылку + * начал добавлять фильтры + + +14.08.2011 Edward V. Emelianoff + + * перешел на mercurial + + +18.07.2011 Edward V. Emelianoff + + - исправить глюки с гистограммами и Хафом + - переписать функции рассчета вершин и нормалей на CUDA + - сделать нормальные пределы на гистограмме (а не [0, 1]) + - скорректировать линейки на графическом окне + + +30.05.2011 Edward V. Emelianoff + + * исправлена ошибка с чтением шапок фитс-файлов + * разделены операции обнаружения пятен и распознавания гартманограммы + * автоматическое распознавание пред- и зафокальных гартманограмм + * сохранение списка координат обнаруженных пятен + + +10.04.2011 Edward V. Emelianoff + + -+частично реализовано сохранение FITSов (и шапок) + -+частично реализовано вращение 3D мышью + + +04.04.2011 Edward V. Emelianoff + + * пункт меню "открыть" в окне 3D (без навигации pgUp/pgDwn) + * прокрутка в редактировании/просмотре шапки фитсов + * пункты меню "открыть в новом окне" и "открыть в 3D" + + +31.03.2011 Edward V. Emelianoff + + * навигация по файлам в текущей директории + + +31.03.2011 Edward V. Emelianoff + +Пре-альфа версия, на ближайшее время реализовать: + - отображение имени файла в строке состояния (а то непонятно, что за файл открыт) + - глюк: "увеличение рамкой" не отменяется + - исправить глюки при открывании нового файла (остаются старые режимы, spots и т.п.) + - предлагать на выбор методы определения центров тяжести (аппр. гауссом, параболой по логарифму, "тупой", параболой по макушке, "тупой" с порогами/весами, аппр. параболоидом...) + - отображать отмеченные треки, границы на гистограмме и т.п. + - редактировать границы треков + - разбор аргументов командной строки + - batch-режим + - вращение 3D мышью + - сохранение FITSов (и шапок) преобразованного изображения + - интерактивный Хаф + - распознавание окружностей + - расчет поверхности зеркала по гартманограммам (случай 1 и пары снимков) + - фильтрация изображений (свертки/Фурье, вейвлеты) + - корреляционный анализ + - визуальное сравнение двух изображений (верхнее - полупрозрачное) + - добавить в 3D NURBS'ы + - "лупа", копипаст, редактирование (с сохранением) + - полная статистика + - вычисление экв. ширины линии на срезе + - отождествление кривых (например, сп. порядков или теллур. линий) и "выпрямление" изображения + - арифметические операции над изображениями + - отображение комплексных изображений (варианты: амплитуда, фаза, Re, Im) и комплексные операции (Фурье, арифм., Фурье-фильтры, градиентные фильтры и т.п.) + - глючит редактирование (где-то лишнее free, не снимается выделение с ячеек) + - сохранение FITSов (и шапок) + - вращение 3D мышью + - исправить глюки с гистограммами и Хафом + - переписать функции рассчета вершин и нормалей на CUDA + - сделать нормальные пределы на гистограмме (а не [0, 1]) + - скорректировать линейки на графическом окне + - при открывании нового изображения старое не удаляется из памяти - УТЕЧКА diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..e69de29 diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..621e94f --- /dev/null +++ b/NEWS @@ -0,0 +1 @@ +none diff --git a/README b/README new file mode 100644 index 0000000..ac7090e --- /dev/null +++ b/README @@ -0,0 +1,6 @@ +Ключи cmake: +DEBUG=1 - отладочный режим +NO_CUDA=1 - не использовать CUDA, даже если она поддерживается системой +PROCESSOR_COUNT=X - в многопоточных задачах использовать X потоков +NO_LEPTONICA=1 - не использовать лептонику, даже если она есть в системе +NO_GSL=1 - не использовать GSL, даже если она есть в системе diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..c24c61c --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,88 @@ +cmake_minimum_required(VERSION 2.6) +if(DEFINED DEBUG) + add_definitions(-DEBUG) +endif() +set(SRC ${CMAKE_SOURCE_DIR}/src) +aux_source_directory(${SRC} SOURCES) +set(NOCUFILE ${SRC}/NOCUDA.c) +set(CUFILE ${SRC}/CUDA.cu) +set(CFLAGS -O3 -Wall -Werror -W -std=c99) +set(LCPATH ${SRC}/locale/ru) +set(PO_FILE ${LCPATH}/messages.po) +set(MO_FILE ${LCPATH}/LC_MESSAGES/${PROJ}.mo) +set(RU_FILE ${LCPATH}/ru.po) +find_package(PkgConfig REQUIRED) +find_package(OpenGL REQUIRED) +find_package(GTK2 REQUIRED) +pkg_check_modules(${PROJ} REQUIRED + #glib-2.0>=2.10 + gtkglext-1.0>=0.7.0 + gtkglext-x11-1.0>=0.7.0 + #gtk+-2.0>=2.6.0 + cfitsio>=3.0 + fftw3>=3.2.0 + ) +include(FindOpenMP) +if(OPENMP_FOUND) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${OpenMP_EXE_LINKER_FLAGS}") +endif() +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -lfftw3_threads") +if(NOT DEFINED NO_LEPTONICA) + pkg_check_modules(LIBLEPT liblept) +endif() +if(NOT DEFINED NO_GSL) + pkg_check_modules(GSL gsl) +endif() +if(NOT DEFINED GSL_VERSION) + message("GSL not found, some mathematics functions wouldn't be avialable") +else() + add_definitions(-DGSL_FOUND) +endif() +if(NOT DEFINED LIBLEPT_VERSION) + message("Leptonica library not found, some functions wouldn't be avialable") +else() + add_definitions(-DLEPTONICA_FOUND) +endif() +if(CUDA_FOUND) + list(REMOVE_ITEM SOURCES ${NOCUFILE}) + list(APPEND CUDA_NVCC_FLAGS --use_fast_math) + cuda_include_directories(include) + cuda_add_executable(${PROJ} ${SOURCES} ${CUFILE} ${PO_FILE} ${MO_FILE} ui.h) + target_link_libraries( ${PROJ} ${${PROJ}_LIBRARIES} + ${GSL_LIBRARIES} ${LIBLEPT_LIBRARIES} + ${CUDA_CUFFT_LIBRARIES} -lcuda) +else(CUDA_FOUND) + find_package(Threads) + add_executable(${PROJ} ${SOURCES} ${PO_FILE} ${MO_FILE} ui.h) + target_link_libraries( ${PROJ} ${${PROJ}_LIBRARIES} + ${GSL_LIBRARIES} ${LIBLEPT_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT} + ) +endif(CUDA_FOUND) +include_directories(${SRC}/include ${${PROJ}_INCLUDE_DIRS} ${GSL_INCLUDE_DIRS} ${LIBLEPT_INCLUDE_DIRS}) +link_directories(${${PROJ}_LIBRARY_DIRS} ${GSL_LIBRARY_DIRS} ${LIBLEPT_LIBRARY_DIRS}) +add_definitions(-DPACKAGE_VERSION=\"0.0.1\" -DGETTEXT_PACKAGE=\"${PROJ}\" + -DLOCALEDIR=\"~/.local/share/locale\" ${CFLAGS}) + +#if(DEFINED DEBUG) + find_package(Gettext REQUIRED) + find_program(GETTEXT_XGETTEXT_EXECUTABLE xgettext) + if(NOT GETTEXT_XGETTEXT_EXECUTABLE ) + message(FATAL_ERROR "xgettext not found") + endif(NOT GETTEXT_XGETTEXT_EXECUTABLE ) +add_custom_command( + OUTPUT ${PO_FILE} + COMMAND ${GETTEXT_XGETTEXT_EXECUTABLE} --from-code=koi8-r ${SOURCES} ${SRC}/${PROJ}.glade -k_ -kN_ -o ${PO_FILE} + DEPENDS ${SOURCES}) +add_custom_command( + OUTPUT ${MO_FILE} + COMMAND ${GETTEXT_MSGFMT_EXECUTABLE} ${RU_FILE} -o ${MO_FILE} + DEPENDS ${RU_FILE}) +add_custom_command( + OUTPUT ui.h + COMMAND gtk-builder-convert ${SRC}/${PROJ}.glade ${PROJ}.ui + COMMAND ${SRC}/scripts/genh ${PROJ}.ui + DEPENDS ${PROJ}.glade) +#endif(DEFINED DEBUG) diff --git a/src/CUDA.cu b/src/CUDA.cu new file mode 100644 index 0000000..c72fe57 --- /dev/null +++ b/src/CUDA.cu @@ -0,0 +1,693 @@ +/* + * CUDA.cu - subroutines for GPU + * + * Copyright 2011 Edward V. Emelianoff + * + * This program 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 2 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ +#define _CUDA_CU_ +#include "include/CUtools.h" + +#include +#include +//#include +//#include +//#include + +const int SHMEMSZ = 16383; // default constants, changed runtime +const int QBLKSZ = 16; // QBLKSZ = sqrt(LBLKSZ) +const int LBLKSZ = 512; + +// static arrays for sines & cosines values +static float *Sin_d = NULL, *Cos_d = NULL; +// array size +static int sincosize = 0; + +cudaError_t CUerr; +inline int CUERROR(char *str){ + if(CUerr != cudaSuccess){ + fprintf(stderr, "%s, %s\n", str, cudaGetErrorString(CUerr)); + return 1; + }else return 0; +} +// error macro (by default - nothing) +#define RETMACRO return +// memory macros +#define CUALLOC(var, size) do{ \ + CUerr = cudaMalloc((void**)&var, size); \ + if(CUERROR("CUDA: can't allocate memory")){ \ + RETMACRO; \ +}}while(0) +#define CUMOV2DEV(dest, src, size) do{ \ + CUerr = cudaMemcpy(dest, src, size, \ + cudaMemcpyHostToDevice); \ + if(CUERROR("CUDA: can't copy data to device")){\ + RETMACRO;} \ +}while(0) +#define CUMOV2HOST(dest, src, size) do{ \ + CUerr = cudaMemcpy(dest, src, size, \ + cudaMemcpyDeviceToHost); \ + if(CUERROR("CUDA: can't copy data to host")){\ + RETMACRO;} \ +}while(0) +#define CUFREE(var) do{cudaFree(var); var = NULL; }while(0) +#define CUFFTCALL(fn) do{ \ + cufftResult fres = fn; \ + if(CUFFT_SUCCESS != fres){ \ + fprintf(stderr, "CUDA fft error %d\n", fres);\ + RETMACRO;} \ +}while(0) + +#ifdef EBUG + #define FNAME() fprintf(stderr, "\n%s (%s, line %d)\n", __func__, __FILE__, __LINE__) + #define DBG(...) do{fprintf(stderr, "%s (%s, line %d): ", __func__, __FILE__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, "\n");} while(0) +#else + #define FNAME() do{}while(0) + #define DBG(...) do{}while(0) +#endif //EBUG + +// getting the videocard parameters +extern "C" void getprops(){ + cudaDeviceProp dP; + CUdevice dev; CUcontext ctx; + cudaGetDeviceProperties(&dP, 0); + cuDeviceGet(&dev,0); + cuCtxCreate(&ctx, 0, dev); + printf("\nDevice: %s, totalMem=%zd, memPerBlk=%zd,\n", dP.name, dP.totalGlobalMem, dP.sharedMemPerBlock); + printf("warpSZ=%d, TPB=%d, TBDim=%dx%dx%d\n", dP.warpSize, dP.maxThreadsPerBlock, + dP.maxThreadsDim[0],dP.maxThreadsDim[1],dP.maxThreadsDim[2]); + printf("GridSz=%dx%dx%d, MemovrLap=%d, GPUs=%d\n", dP.maxGridSize[0], + dP.maxGridSize[1],dP.maxGridSize[2], + dP.deviceOverlap, dP.multiProcessorCount); + printf("canMAPhostMEM=%d\n", dP.canMapHostMemory); + printf("compute capability %d.%d.\n\n", dP.major, dP.minor); + if(dP.major > 1){ + // SHMEMSZ = 49151; QBLKSZ = 32; LBLKSZ = 1024; + } + size_t theFree, theTotal; + CUresult aaa = cuMemGetInfo( &theFree, &theTotal ); + printf("CARD returns(err=%d): free mem:%zd, total mem:%zd\n", aaa, theFree, theTotal); + cuCtxDetach(ctx); +} + +// normalisation of array arr with size arrsize +__global__ void normalize_vec(float *arr, int arrsize){ + __shared__ float max[LBLKSZ]; + int idx = threadIdx.x; + int blksize = (arrsize + blockDim.x - 1) / blockDim.x; + int b_beg = idx * blksize; + if(b_beg >= arrsize) return; + int b_end = b_beg + blksize; + if(b_end > arrsize) b_end = arrsize; + int i; float *ptr = &arr[b_beg]; + float mm = *ptr++; + for(i = b_beg +1 ; i < b_end; i++, ptr++) + if(mm < *ptr) mm = *ptr; + max[idx] = mm; + __syncthreads(); + if(idx == 0){ + mm = max[0]; + for(i = 1; i < LBLKSZ; i++) + if(mm < max[i]) mm = max[i]; + max[0] = mm; + } + __syncthreads(); + ptr = &arr[b_beg]; + mm = max[0]; + if(mm != 0.f) + for(i = b_beg ; i < b_end; i++, ptr++) *ptr /= mm; +} +/* +__global__ void fill_zeros(float *arr, int arrsize, int W, int H){ + int x = blockIdx.x * blockDim.x + threadIdx.x; + int y = blockIdx.y * blockDim.y + threadIdx.y; + if(x > W || y > H) return; + arr[x + y*W] = 0.f; +}*/ + +// kernel of function for sin/cos array initialisation +__global__ void fill_sincos(int angles, + float *Sin_d, float *Cos_d, + float anglestep, float conv){ + int k = blockIdx.x * blockDim.x + threadIdx.x; + if(k >= angles) return; + float theta = ((float)k / anglestep - 90.f) * conv; + sincosf(theta, &Sin_d[k], &Cos_d[k]); +} + +// initialisation of sin/cos arrays +extern "C" int init_sincos(int angles){ + #undef RETMACRO + #define RETMACRO return 0 + // the value reciprocal for angle step + float anglestep = (float)angles / 270.f; + float conv = M_PI / 180.f; + int blks = (angles + QBLKSZ - 1) / QBLKSZ; + int threads = LBLKSZ; + // first time we initialize arrays + if(!Sin_d || !Cos_d || angles != sincosize){ + CUFREE(Cos_d); + CUFREE(Sin_d); + CUALLOC(Sin_d, angles*sizeof(float)); + CUALLOC(Cos_d, angles*sizeof(float)); + fill_sincos<<>>(angles, Sin_d, Cos_d, anglestep, conv); + sincosize = angles; + } + cudaThreadSynchronize(); + return 1; + #undef RETMACRO + #define RETMACRO return +} + +/* + * Lines Hough transform kernel + * ima_d - device array with the image + * imW, imH, min, max - width, height of the image and extreme values of its histogram + * Sin_d, Cos_d - device array with sines and cosines of angles (-90..180degr increments 270/angles) + * Rmax - the maximum range for the desired lines + * angles - the number of angles in the range -90 .. 180 + * treshold - lower threshold of intensity (in relative units: I=tres*(max-min)+min) for inclusion of point into array + * hough_d - output array with Hough transform + */ +__global__ void fill_lin_hough_array(float *ima_d, + int imW, int imH, + float min, float max, + float *Sin_d, float *Cos_d, + int Rmax, int angles, float treshold, + float *hough_d){ + int xi = blockIdx.x * blockDim.x + threadIdx.x; + int yi = blockIdx.y * blockDim.y + threadIdx.y; + int i = xi + imW * yi; + float x = (float)xi; + float y = (float)yi; + int k, R; + if(xi >= imW || yi >= imH) return; + float wd = max-min; if(wd == 0.f) wd = 1.f; + float ima = (ima_d[i]-min)/wd; + if(ima > treshold){ + for(k = 0; k < angles; k++){ + // R = x*cos(theta) + y*sin(theta) + R = (int)(0.5f + x * Cos_d[k] + y * Sin_d[k]); + // THIS IS VERY BAD, BUT atomicAdd doesn't work in old devices + if(R > 0 && R < Rmax) hough_d[R + Rmax*k] += ima; + //if(R > 0 && R < Rmax) atomicAdd(&hough_d[R + Rmax*k], ima); + } + } +} + +/* + * Build Hough transform to find lines + * Input: + * ima - the image data + * min, max - range of the data in it + * imW, imH - image width and height + * Rmax - the maximum value of R + * Angles - the array size of angles (the angle of pitch is 180/angles degrees) + * Output: + * hough - array initialized by an external function, + * in which the Hough transform will be + * !!! array must be initialized with zeros before calling this function + * Output array is normalized to unity + */ +extern "C" int fill_hough_lines(float *ima, float min, float max, int imW, int imH, + int Rmax, int angles, float *hough){ + #undef RETMACRO + #define RETMACRO do{ ret = 0; goto free_all; }while(0) + int sz, ret = 1; + int lblksz = LBLKSZ; + float *ima_d = NULL, *hough_d = NULL; + float treshold = 0.1f; + sz = imW * imH; + getprops(); + dim3 blkdim(QBLKSZ, QBLKSZ); + dim3 griddim((imW+QBLKSZ-1)/QBLKSZ, (imH+QBLKSZ-1)/QBLKSZ); +// dim3 hgriddim((Rmax+QBLKSZ-1)/QBLKSZ, (angles+QBLKSZ-1)/QBLKSZ); + if(!init_sincos(angles)) RETMACRO; + CUALLOC(ima_d, sz*sizeof(float)); + CUMOV2DEV(ima_d, ima, sz*sizeof(float)); + sz = Rmax * angles; + CUALLOC(hough_d, sz*sizeof(float)); + cudaMemset(hough_d, 0, sz*sizeof(float)); +// fill_zeros<<>>(hough_d, sz, Rmax, angles); + cudaThreadSynchronize(); + CUMOV2DEV(hough_d, hough, sz*sizeof(float)); + fill_lin_hough_array<<>>(ima_d, imW,imH, min,max, Sin_d,Cos_d, + Rmax, angles, treshold, hough_d); + cudaThreadSynchronize(); + normalize_vec<<<1, lblksz>>>(hough_d, sz); + cudaThreadSynchronize(); + CUMOV2HOST(hough, hough_d, sz*sizeof(float)); +free_all: + CUFREE(hough_d); + CUFREE(ima_d); + return ret; + #undef RETMACRO + #define RETMACRO return +} + +/* + * Kernels of the threshold filtering + * in, out - in and out + * stepfn - a pointer to a function of conversion + * sizex, sizey - image size + * min - the minimum intensity + * wd - range of the data + * step - a step for stepfn + */ +// uniform intensity distribution +__global__ void Funiform(float *in, int sizex, int sizey, float min, float step){ + int xi = blockIdx.x * blockDim.x + threadIdx.x; + int yi = blockIdx.y * blockDim.y + threadIdx.y; + int i = xi + yi*sizex; + if(xi >= sizex || yi >= sizey) return; + in[i] = floor((in[i]-min)/step); +} +// logarithm distribution +__global__ void Flog(float *in, int sizex, int sizey, float min, float step){ + int xi = blockIdx.x * blockDim.x + threadIdx.x; + int yi = blockIdx.y * blockDim.y + threadIdx.y; + int i = xi + yi*sizex; + if(xi >= sizex || yi >= sizey) return; + in[i] = floor(logf(in[i]-min+1.f)/step); +} +// exponential distribution +__global__ void Fexp(float *in, int sizex, int sizey, float min, float wd, float step){ + int xi = blockIdx.x * blockDim.x + threadIdx.x; + int yi = blockIdx.y * blockDim.y + threadIdx.y; + int i = xi + yi*sizex; + if(xi >= sizex || yi >= sizey) return; + in[i] = floor(expf((in[i]-min)/wd)/step); +} +// distribution of a square root +__global__ void Fsqrt(float *in, int sizex, int sizey, float min, float step){ + int xi = blockIdx.x * blockDim.x + threadIdx.x; + int yi = blockIdx.y * blockDim.y + threadIdx.y; + int i = xi + yi*sizex; + if(xi >= sizex || yi >= sizey) return; + in[i] = floor(sqrtf(in[i]-min)/step); +} +// distribution of a x^2 +__global__ void Fpow(float *in, int sizex, int sizey, float min, float step){ + int xi = blockIdx.x * blockDim.x + threadIdx.x; + int yi = blockIdx.y * blockDim.y + threadIdx.y; + int i = xi + yi*sizex; + if(xi >= sizex || yi >= sizey) return; + in[i] = floor((in[i]-min)*(in[i]-min)/step); +} + +// functions for calculation of output scale +float Suniform(float in, float min, float wd, float step){ + return step*in + min; +} +float Slog(float in, float min, float wd, float step){ + return expf(in*step) + min - 1.f; +} +float Sexp(float in, float min, float wd, float step){ + return wd*logf(in*step) + min; +} +float Ssqrt(float in, float min, float wd, float step){ + return in*in*step*step + min; +} +float Spow(float in, float min, float wd, float step){ + return sqrtf(in*step) + min; +} + +/* + * Threshold filtering ("posterization") + * Input: + * ima - picture (free() must be executed in the caller) + * f - filter: + * f-> w - number of levels of posterization, [2.255] + * f-> h - type of posterization (0 - uniform) + * sizex, sizey - image size + * min, max - minimum and maximum intensity of the image + * Output: + * result - filtered image, the memory is allocated in this procedure + * scale - the scale of intensities, the memory is allocated here (if the scale!=NULL) + * + * TODO: save the result in the char, not float; learn display function + */ +extern "C" int StepFilter(float *ima, float **result, + Filter *f, int sizex, int sizey, + float min, float max, float **scale){ + #undef RETMACRO + #define RETMACRO do{ ret = 0; goto free_all; }while(0) + int ret = 1; + float wd = max - min; + int y; + float Nsteps = (float)f->w; // number of intervals + float step; + float *in=NULL; // image and result array + int sz = sizex*sizey*sizeof(float); + dim3 blkdim(QBLKSZ, QBLKSZ); + dim3 griddim((sizex+QBLKSZ-1)/QBLKSZ, (sizey+QBLKSZ-1)/QBLKSZ); + *result = (float*)malloc(sz); + if(!result) RETMACRO; + float (*scalefn)(float,float,float,float); + if(f->w < 2 || f->w > 255) return 0; + if(wd == 0.f) return 0; + CUALLOC(in, sz); + CUMOV2DEV(in, ima, sz); + switch(f->h){ // filter type + case LOG: // logarithm + scalefn = Slog; + step = logf(max-min+1.f)/Nsteps; + Flog<<>>(in, sizex, sizey, min, step); + break; + case EXP: // exponential + scalefn = Sexp; + step = expf(1.f)/Nsteps; + Fexp<<>>(in, sizex, sizey, min, wd, step); + break; + case SQRT: // square root + scalefn = Ssqrt; + step = sqrtf(wd)/Nsteps; + Fsqrt<<>>(in, sizex, sizey, min, step); + break; + case POW: // power of two + scalefn = Spow; + step = wd*wd/Nsteps; + Fpow<<>>(in, sizex, sizey, min, step); + break; + default: // uniform + scalefn = Suniform; + step = wd/Nsteps; + Funiform<<>>(in, sizex, sizey, min, step); + } + cudaThreadSynchronize(); + CUMOV2HOST(*result, in, sz); + if(scale){ + int M = f->w; + *scale = (float*)calloc(M, sizeof(float)); + if(*scale) for(y = 0; y < M; y++){ + (*scale)[y] = scalefn(y+1,min,wd,step); + } + } +free_all: + CUFREE(in); + return ret; + #undef RETMACRO + #define RETMACRO return +} + + +/* + * A set of functions for constructing differential filters + */ +int p2oi(int i){ + unsigned int v = (unsigned int)i - 1; + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + v++; + return (int) v; +} +int nextpow2(int i, int j){ + int p1 = p2oi(i), p2 = p2oi(j); + return (p1 > p2)? p1 : p2; +} +// multiplication of two complex matrices with size x size +// result in the entry of the first matrix +__global__ void ComplexMul(cufftComplex *inout, cufftComplex *in, int size){ + int xi = blockIdx.x * blockDim.x + threadIdx.x; + int yi = blockIdx.y * blockDim.y + threadIdx.y; + if(xi >= size || yi >= size) return; + int i = xi + yi*size; + cufftComplex a = inout[i], b = in[i]; + inout[i].x = a.x * b.x - a.y * b.y; + inout[i].y = a.x * b.y + a.y * b.x; +} + +// restore coordinates of the Fourier transform +__global__ void fftshift(int size, float *m){ + int h = size/2; + int xi = blockIdx.x * blockDim.x + threadIdx.x; + int yi = blockIdx.y * blockDim.y + threadIdx.y; + if(xi >= h || yi >= h) return; + // k - point in left upper quadrant, k1 - in right upper + int k = yi * size+xi, k1 = k + h; + // p - point in right lower quadrant, p1 - in left lower + int p = k + (size+1)*h, p1 = k1 + (size-1)*h; + float tmp; + tmp = m[k]; m[k] = m[p]; m[p] = tmp; + tmp = m[k1]; m[k1] = m[p1]; m[p1] = tmp; +} +// data copying float->cufftReal (need because different sizes of picture and Fourier image) +__global__ void f2r(cufftReal *out, float *in, int sizex, int sizey, int size2){ + int xi = blockIdx.x * blockDim.x + threadIdx.x; + int yi = blockIdx.y * blockDim.y + threadIdx.y; + if(xi >= sizex || yi >= sizey) return; + out[xi+yi*size2] = (cufftReal) in[xi+yi*sizex]; +} +// data copying cufftReal->float +__global__ void r2f(float *out, cufftReal *in, int sizex, int sizey, int size2){ + int xi = blockIdx.x * blockDim.x + threadIdx.x; + int yi = blockIdx.y * blockDim.y + threadIdx.y; + if(xi >= sizex || yi >= sizey) return; + out[xi+yi*sizex] = (float) in[xi+yi*size2]; +} + +/* + * The kernel of the Laplacian of Gaussian + * Output: + * mask - the filled array + * Input: + * size - array size (size x size) + * x0, x1 - array bounds on x: [x0, x1) (outside this array filled by zeros) + * y0, y1 - -//- on y + * half - half the size of the array + * ss - normalizing factor + * sx2, sy2 - the variance of the filter in x and y + */ +__global__ void LGf_kernel(cufftReal *mask, int size, int x0, int x1, + int y0, int y1, float half, float ss, + float sx2, float sy2){ + int xi = blockIdx.x * blockDim.x + threadIdx.x + x0; + int yi = blockIdx.y * blockDim.y + threadIdx.y + y0; + if(xi >= x1 || yi >= y1) return; + int i = xi + yi*size; + float x2 = (float)xi + half; + float y2 = (float)yi + half; + x2 = x2*x2 / sx2; y2 = y2*y2 / sy2; + mask[i] = (cufftReal)(ss * ((x2-1.f)/sx2+(y2-1.f)/sy2)*expf(-(x2+y2)/2.f)); +} +// The kernel of Gaussian filter +__global__ void Gf_kernel(cufftReal *mask, int size, int x0, int x1, + int y0, int y1, float half, float ss, + float sx2, float sy2){ + int xi = blockIdx.x * blockDim.x + threadIdx.x + x0; + int yi = blockIdx.y * blockDim.y + threadIdx.y + y0; + if(xi >= x1 || yi >= y1) return; + int i = xi + yi*size; + float x2 = (float)xi + half; + float y2 = (float)yi + half; + x2 = x2*x2 / sx2; y2 = y2*y2 / sy2; + mask[i] = (cufftReal)(ss * expf(-(x2+y2)/2.f)); +} +/* + * Building mask of Gaussian or Laplasian of Gaussian + * Output: + * mask - filter array + * Input: + * size - mask size (size x size) + * f - filter parameters + */ +void build_GLG_filter(cufftReal *mask, int size, Filter *f){ + int y0=0,y1=size, x0=0, x1=size; + float sx2 = f->sx * f->sx, sy2 = f->sy * f->sy; + float half; + dim3 blkdim(QBLKSZ, QBLKSZ); + dim3 griddim((size+QBLKSZ-1)/QBLKSZ, (size+QBLKSZ-1)/QBLKSZ); + if(f->w < size && f->w > 0){ + x0 = (size - f->w + 1) / 2; + x1 = x0 + f->w; + } + if(f->h < size && f->h > 0){ + y0 = (size - f->h + 1) / 2; + y1 = y0 + f->h; + } + half = -(float)size / 2.f; + float ss = 3.f / half / half / sqrt(-half); + switch(f->FilterType){ + case LAPGAUSS: + LGf_kernel<<>>(mask, size, x0,x1, y0,y1, half, ss, sx2, sy2); + break; + case GAUSS: + Gf_kernel<<>>(mask, size, x0,x1, y0,y1, half, ss, sx2, sy2); + break; + default: + fprintf(stderr, "Error: bad filter\n"); + } + cudaThreadSynchronize(); + DBG("size=%d, x0=%d,x1=%d, y0=%d,y1=%d, half=%g, ss=%g, sx2=%g, sy2=%g", + size, x0,x1, y0,y1, half, ss, sx2, sy2); +} + +/* + * Building of elementary filter mask + * Output: + * mask - filter array + * Input: + * size - mask size (size x size) + * f - filter parameters + */ +void build_S_filter(cufftReal *mask, int size, Filter *f){ + int y, a0, a1; + float hh, Y, pt = 0.f; + a0 = (size - 2) / 2; + a1 = a0 + 3; + hh = -(float)(size / 2); + float ss = 1.f / (M_PI*2.f) / hh / hh / sqrtf(-hh); + Y = -1.f; + for(y = a0; y < a1; y++, Y+=1.f){ + float X = -1.f; + int str, x; + str = y * size; + for(x = a0; x < a1; x++, X+=1.f){ + switch(f->FilterType){ + case SOBELH: + pt = -X*(2.f-fabs(Y)); + break; + case SOBELV: + pt = -Y*(2.f-fabs(X)); + break; + case PREWITTH: + pt = X; + break; + case PREWITTV: + pt = Y; + break; + } + cufftReal tmppar = (cufftReal)ss*pt; + cudaMemcpy(&mask[str + x], &tmppar, sizeof(cufftReal), cudaMemcpyHostToDevice); + } + } +} +/* + * Convolution filtering (convolution by FFT) + * Input: + * ima - picture, that need to be filtering + * f - filter parameters + * Output: + * result - memory area, allocated by this function, + * where the filtered picture to be store + * return: TRUE if the filtering succeed + */ +extern "C" int DiffFilter(float *ima, float **result, + Filter *f, int sizex, int sizey){ + #undef RETMACRO + #define RETMACRO do{ ret = 0; goto free_all; }while(0) + int ssize, ret = 0, size2; + float *tmp; + size2 = nextpow2(sizex, sizey); + ssize = size2 * size2; // Fourier image size + dim3 blkdim(QBLKSZ, QBLKSZ); + dim3 griddim((size2+QBLKSZ-1)/QBLKSZ, (size2+QBLKSZ-1)/QBLKSZ); + dim3 halfgriddim((size2/2+QBLKSZ-1)/QBLKSZ, (size2/2+QBLKSZ-1)/QBLKSZ); + dim3 imgriddim((sizex+QBLKSZ-1)/QBLKSZ, (sizey+QBLKSZ-1)/QBLKSZ); + if(!result || !*result || !ima || !f){ + fprintf(stderr, "DiffFilter: bad parameters\n"); + return 0; + } + cufftHandle plan; + cufftComplex *Fmask=NULL, *Fimg=NULL; + cufftReal *mask=NULL, *img=NULL, *resm=NULL; + #ifdef EBUG + getprops(); + #endif + // Allocate memory for new objects + DBG("allocate"); + CUALLOC(img, ssize*sizeof(cufftReal)); + // fill it zeros + cudaMemset(img, 0, ssize*sizeof(cufftReal)); + // copy ima -> img + DBG("copy image to dev"); + CUALLOC(tmp, sizex*sizey*sizeof(float)); + CUMOV2DEV(tmp, ima, sizex*sizey*sizeof(float)); + f2r<<>>(img, tmp, sizex, sizey, size2); + cudaThreadSynchronize(); + CUFREE(tmp); + CUALLOC(Fimg, ssize*sizeof(cufftComplex)); + // make FFT + DBG("doing image FFT"); + CUFFTCALL(cufftPlan2d(&plan, size2, size2, CUFFT_R2C)); + CUFFTCALL(cufftExecR2C(plan, img, Fimg)); + CUFREE(img); + DBG("allocate"); + CUALLOC(mask, ssize*sizeof(cufftReal)); + cudaMemset(mask, 0, ssize*sizeof(cufftReal)); + CUALLOC(Fmask, ssize*sizeof(cufftComplex)); + switch(f->FilterType){ + case LAPGAUSS: + case GAUSS: + build_GLG_filter(mask, size2, f); + break; + case SOBELH: + case SOBELV: + case PREWITTH: + case PREWITTV: + build_S_filter(mask, size2, f); + break; + default: + fprintf(stderr, "Error: bad filter\n"); + RETMACRO; + } + // swap filter quadrants + fftshift<<>>(size2, mask); + cudaThreadSynchronize(); + // make FFT + DBG("doing filter FFT"); + CUFFTCALL(cufftExecR2C(plan, mask, Fmask)); + CUFFTCALL(cufftDestroy(plan)); + CUFREE(mask); + // make convolution in Fourier space + DBG("multiplication"); + ComplexMul<<>>(Fimg, Fmask, size2); + cudaThreadSynchronize(); + CUFREE(Fmask); + // Inverse FFT + DBG("doing inverse FFT"); + CUALLOC(resm, ssize*sizeof(cufftReal)); + CUFFTCALL(cufftPlan2d(&plan, size2, size2, CUFFT_C2R)); + CUFFTCALL(cufftExecC2R(plan, Fimg, resm)); + CUFFTCALL(cufftDestroy(plan)); + CUFREE(Fimg); + DBG("allocate"); + CUALLOC(tmp, ssize*sizeof(float)); + *result = (float*)calloc(sizex*sizey, sizeof(float)); + if(!*result) RETMACRO; + // copy iFFT -> res + DBG("copy to host"); + r2f<<>>(tmp, resm, sizex, sizey, size2); + cudaThreadSynchronize(); + CUMOV2HOST(*result, tmp, sizex*sizey*sizeof(float)); + ret = 1; +free_all: + CUFREE(Fmask); CUFREE(Fimg); CUFREE(img); + CUFREE(mask); CUFREE(resm); CUFREE(tmp); + #ifdef EBUG + getprops(); + #endif + return ret; + #undef RETMACRO + #define RETMACRO return +} +extern "C" int MedFilter(float *ima, float **result, Filter *f, int sizex, int sizey){return 0;} +extern "C" int GradFilterSimple(float *ima, float **result, Filter *f, int sizex, int sizey){return 0;} diff --git a/src/NOCUDA.c b/src/NOCUDA.c new file mode 100644 index 0000000..fef4410 --- /dev/null +++ b/src/NOCUDA.c @@ -0,0 +1,816 @@ +// NOCUDA.c - CPU-variants when there's no CUDA +// +// Copyright 2011 Edward V. Emelianoff +// +// This program 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 2 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 this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +// MA 02110-1301, USA. + +#include "fitsview.h" +#include "gtk.h" +#include "CUtools.h" +#include +#include + +static float *Sin_d = NULL, *Cos_d = NULL; +static int sincosize = 0; + +#ifndef THREAD_NUMBER + #define THREAD_NUMBER 2 // default - 2 threads +#endif + +typedef struct{ + float *image; + float min; + float max; + float *hough; + int w; + int Y0; + int Y1; + int Rmax; + int angles; + pthread_mutex_t *mutex; +}Hough_kernel_args; + +// sin/cos ini, [-90,+180) +void init_sincos(int angles){ + float anglestep = (float)angles / 270.; // step by an angle + float conv = M_PI / 180.; + float *cp, *sp; + int k; + if(!Sin_d || !Cos_d || angles != sincosize){ // first time initialize arrays + free(Sin_d); + free(Cos_d); + Sin_d = malloc(angles * sizeof(float)); + Cos_d = malloc(angles * sizeof(float)); + cp = Cos_d; sp = Sin_d; + for(k = 0; k < angles; k++){ // fill arrays from -90 to +90 degr in radians + float theta = ((float)k / anglestep - 90.) * conv; + *sp++ = sinf(theta); + *cp++ = cosf(theta); + //sincosf(theta, sp++, cp++); + } + sincosize = angles; + } +} + +// Line Hough transform kernel +void *fill_lin_hough_array(void *data){ + Hough_kernel_args *HD = (Hough_kernel_args*) data; + float *ima = HD->image, *hough = HD->hough; + float min = HD->min, wd = HD->max - min; + pthread_mutex_t *mutex = HD->mutex; + int Y0 = HD->Y0, Y1 = HD->Y1, imW = HD->w, Rmax = HD->Rmax, angles = HD->angles; + int i, j, k, Y, R; + for(j = Y0; j < Y1; j++){ + for(i = 0.; i < imW; i++, ima++){ + float imdata = (*ima - min) / wd; + if(imdata > 0.1){ + Y = 0; + pthread_mutex_lock(mutex); + for(k = 0; k < angles; k++, Y+=Rmax){ + // R = x*cos(theta) + y*sin(theta) + R = (int)(0.5 + i*Cos_d[k] + j*Sin_d[k]); + if(R > 0 && R < Rmax) hough[R + Y] += imdata; + } + pthread_mutex_unlock(mutex); + } + } + } + return NULL; +} +/* + * Build of a Hough transform to find lines + * Input: + * ima - the image data + * min, max - range of the data in it + * imW, imH - image width and height + * Rmax - the maximum value of R + * angles - the array size of angles (the angle step is 180/angles degrees) + * Output: + * hough - initialized by an external function array for Hough transform + * !!! array must be initialized with zeros before calling this function !!! + * Output array is normalized to unity + */ +int fill_hough_lines(float *ima, float min, float max, int imW, int imH, int Rmax, int angles, float *hough){ + int i, Y0, Y1, dY; + float *hptr = hough, hmax; + Hough_kernel_args HD[THREAD_NUMBER]; + pthread_t threads[THREAD_NUMBER]; + pthread_mutex_t mutex; + pthread_mutex_init(&mutex, NULL); + init_sincos(angles); + dY = (imH + THREAD_NUMBER/2) / THREAD_NUMBER; + Y0 = 0; Y1 = dY; + for(i = 0; i < THREAD_NUMBER; i++, Y0+=dY, Y1+=dY){ + if(Y0 >= imW) break; + if(Y1 > imW) Y1 = imW; + HD[i].min = min; HD[i].max = max; + HD[i].image = ima; HD[i].hough = hough; + HD[i].Y0 = Y0; HD[i].Y1 = Y1; HD[i].w = imW; + HD[i].Rmax = Rmax; HD[i].angles = angles; + HD[i].mutex = &mutex; + pthread_create(&threads[i], NULL, fill_lin_hough_array, &HD[i]); + } + for(i = 0; i < THREAD_NUMBER; i++) + pthread_join(threads[i], NULL); + pthread_mutex_destroy(&mutex); + hmax = *hough++; + Y1 = Rmax * angles; + for(i = 1; i < Y1; i++, hough++) + if(*hough > hmax) hmax = *hough; + hough = hptr; + for(i = 0; i < Y1; i++, hough++) + *hough /= hmax; + return 1; +} + + + + +// from http://graphics.stanford.edu/%7Eseander/bithacks.html#RoundUpPowerOf2 +int nextpow2(int i, int j){ + inline int p2oi(int i){ + unsigned int v = (unsigned int)i - 1; + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + v++; + return (int) v; + } + int p1 = p2oi(i), p2 = p2oi(j); + return MAX(p1, p2); +} + +// FFT +fftw_plan fftimg, fftmask, ifft; +// FFT buffrers for image & filter +fftw_complex *Fmask = NULL, // FFT of a filter + *Fimg = NULL; // picture FFT +static double *mask = NULL, // filter mask + *img = NULL, // picture + *resm = NULL; + +void fftshift(int size, double *m){ + int h = size/2, ss, ss1, i, j, k, l, p; + double tmp; + ss = (2*h+1)*h; + ss1 = (2*h-1)*h; + for(j = 0; j < h; j++){ + k = j * size; + l = k + h; + for(i = 0; i < h; i++, k++, l++){ + p = k + ss; + tmp = m[k]; m[k] = m[p]; m[p] = tmp; + p = l + ss1; + tmp = m[l]; m[l] = m[p]; m[p] = tmp; + } + } +} + +// Lapgauss mask building +void build_LG_filter(int size, Filter *f){ + int y, y0=0,y1=size, x0=0, x1=size; + double sx2 = f->sx * f->sx, sy2 = f->sy * f->sy; + double hw, hh; + #ifdef EBUG + double t0=dtime(); + #endif + if(f->w < size && f->w > 0){ + x0 = (size - f->w + 1) / 2; + x1 = x0 + f->w; + } + if(f->h < size && f->h > 0){ + y0 = (size - f->h + 1) / 2; + y1 = y0 + f->h; + } + hh = -(double)size / 2.; + hw = -(double)size / 2.+(double)x0; + DBG("y0=%d, y1=%d, hw=%g, hh=%g",y0,y1,hw,hh); + double ss = 3. / hh / hh / sqrt(-hh); +// double ss = 1./sqrt(2*M_PI*f->sx*f->sy); + #pragma omp parallel for + for(y = y0; y < y1; y++){ + double X, Y, y2, x2, R; + int str, x; + X = hw; + str = y * size; + Y = ((double)y) + hh; + y2 = Y*Y/sy2; + for(x = x0; x < x1; x++, X+=1.){ + x2 = X*X/sx2; + R = x2 + y2; + mask[str + x] =ss * ((x2-1.)/sx2+(y2-1.)/sy2)*exp(-R/2.); + } + } + DBG("time=%f\n", dtime()-t0); +} + +// Gaussian mask building/ +void build_G_filter(int size, Filter *f){ + int y, y0=0,y1=size, x0=0, x1=size; + double sx2 = f->sx * f->sx, sy2 = f->sy * f->sy; + double hw, hh; + #ifdef EBUG + double t0=dtime(); + #endif + if(f->w < size && f->w > 0){ + x0 = (size - f->w + 1) / 2; + x1 = x0 + f->w; + } + if(f->h < size && f->h > 0){ + y0 = (size - f->h + 1) / 2; + y1 = y0 + f->h; + } + hh = -(double)size / 2.; + hw = -(double)size / 2.+(double)x0; + DBG("y0=%d, y1=%d, hw=%g, hh=%g",y0,y1,hw,hh); + double ss = 1. / (M_PI*2.) / sx2 / sy2 / hh / hh; + #pragma omp parallel for + for(y = y0; y < y1; y++){ + double X, Y, y2, x2, R; + int str, x; + X = hw; + str = y * size; + Y = ((double)y) + hh; + y2 = Y*Y/sy2; + for(x = x0; x < x1; x++, X+=1.){ + x2 = X*X/sx2; + R = x2 + y2; + mask[str + x] =ss * exp(-R/2.); + } + } + DBG("time=%f\n", dtime()-t0); +} + +// Elementary filter mask building +void build_S_filter(int size, Filter *f){ + int y, a0, a1; + double hh, Y, pt = 0.; + a0 = (size - 2) / 2; + a1 = a0 + 3; + hh = -(double)(size / 2); + double ss = 1. / (M_PI*2.) / hh / hh / sqrt(-hh); + Y = -1.; + for(y = a0; y < a1; y++, Y+=1.){ + double X = -1.; + int str, x; + str = y * size; + for(x = a0; x < a1; x++, X+=1.){ + switch(f->FilterType){ + case SOBELH: + pt = -X*(2.-fabs(Y)); + break; + case SOBELV: + pt = -Y*(2.-fabs(X)); + break; + case PREWITTH: + pt = X; + break; + case PREWITTV: + pt = Y; + break; + /* case : + pt = + break; + case : + pt = + break; + case : + pt = + break; + case : + pt = + break; + case : + pt = + break;*/ + } + mask[str + x] = ss*pt; + } + } + //mask[size*size/2]=0.; +} + +/* + * Filtering by convolution with a filter + * Input: + * ima - a pointer to picture data, which should be filtered + * f - filter parameters + * Output: + * result - allocated by this function memory area where + * the filtered image is placed + * Returns TRUE, if the filter succeeded + */ +int DiffFilter(float *ima, + float **result, + Filter *f, + int sizex, + int sizey + ){ + int ssize, i, j, k, l; + static int fftw_ini = 0; + int size2; + #ifdef EBUG + double t0 = dtime(); + #endif + size2 = nextpow2(sizex, sizey); + ssize = size2 * size2; // FFT image size + if(!fftw_ini) + if(!(fftw_ini = fftw_init_threads())){ + //g_err(_("FFTW error"); + return FALSE; + } + void free_all(){ + _FREE(mask); _FREE(Fmask); _FREE(img); + _FREE(Fimg); _FREE(resm); + } + free_all(); + DBG("img (%d x %d) -> (%d x %d), time=%f\n", sizex,sizey, size2,size2, dtime()-t0); + // allocate memory for objects + img = (double*)calloc(ssize, sizeof(double)); + Fimg = (fftw_complex*)calloc(ssize, sizeof(fftw_complex)); + if(!img || !Fimg){free_all(); return FALSE;} + // define direct FFT + fftw_plan_with_nthreads(THREAD_NUMBER); + fftimg = fftw_plan_dft_r2c_2d(size2, size2, img, Fimg, FFTW_ESTIMATE); + // copy ima -> img + for(j = 0; j < sizey; j++){ + k = j * size2; + l = j * sizex; + for(i = 0; i < sizex; i++, k++, l++) + img[k] = ima[l]; + } + fftw_execute(fftimg); + _FREE(img); + // build filter + DBG("build filter, time=%f\n", dtime()-t0); + mask = (double*)calloc(ssize, sizeof(double)); + Fmask = (fftw_complex*)calloc(ssize, sizeof(fftw_complex)); + if(!mask || !Fmask){free_all(); return FALSE;} + switch(f->FilterType){ + case LAPGAUSS: + build_LG_filter(size2, f); + break; + case GAUSS: + build_G_filter(size2, f); + break; + case SOBELH: + case SOBELV: + case PREWITTH: + case PREWITTV: + build_S_filter(size2, f); + break; + } + // define filter FFT + fftw_plan_with_nthreads(THREAD_NUMBER); + fftmask = fftw_plan_dft_r2c_2d(size2, size2, mask, Fmask, FFTW_ESTIMATE); + fftw_execute(fftmask); + _FREE(mask); + // filtered picture: + DBG("filter image, time=%f\n", dtime()-t0); + resm = (double*)calloc(ssize, sizeof(double)); + if(!resm){free_all(); return FALSE;} + // define inverse FFT + fftw_plan_with_nthreads(THREAD_NUMBER); + ifft = fftw_plan_dft_c2r_2d(size2, size2, Fimg, resm, FFTW_ESTIMATE); + // DON'T PARALLEL THIS, it will be slower + for(i=0; i result + for(j = 0; j < sizey; j++){ + k = j * size2; + l = j * sizex; + for(i = 0; i < sizex; i++, k++, l++) + tmp[l] = resm[k]; + } + _FREE(resm); + fftw_destroy_plan(fftimg); + fftw_destroy_plan(ifft); + fftw_destroy_plan(fftmask); + fftw_cleanup_threads(); + DBG("time=%f\n", dtime()-t0); + return TRUE; +} + +/* + * Simple gradient filter based on two Sobel filters + * output = sqrt(SobelH(input)^2+SobelV(input)^2) + */ +int GradFilterSimple(float *ima, float **result, Filter *f, int w, int h){ + float *dst1, *dst2; + int res = FALSE, y; + #ifdef EBUG + double t0 = dtime(); + #endif + f->FilterType = SOBELH; + res = DiffFilter(ima, &dst1, f, w, h); + if(!res) return FALSE; + f->FilterType = SOBELV; + res = DiffFilter(ima, &dst2, f, w, h); + if(!res){free(dst1); return FALSE;} + *result = dst1; + #pragma omp parallel for + for(y = 0; y < h; y++){ + int x; + float *in, *out; + in = dst2 + y*w; + out = dst1 + y*w; + for(x = 0; x < w; x++, in++, out++) + *out = sqrtf((*in)*(*in) + (*out)*(*out)); + } + free(dst2); + DBG("time=%f\n", dtime()-t0); + return TRUE; +} + +/* + * Quick median functions stolen from + * + * Fast median search: an ANSI C implementation + * Nicolas Devillard - ndevilla AT free DOT fr + * July 1998 + */ +#define PIX_SORT(a,b) { if ((*a)>(*b)) ELEM_SWAP((a),(b)); } +#define ELEM_SWAP(a,b) { register float *t=(a);(a)=(b);(b)=t; } +float opt_med3(float **p, int n __attribute__((unused))){ + PIX_SORT(p[0],p[1]) ; PIX_SORT(p[1],p[2]) ; PIX_SORT(p[0],p[1]) ; + return(*p[1]) ; +}float opt_med5(float **p, int n __attribute__((unused))){ + PIX_SORT(p[0],p[1]) ; PIX_SORT(p[3],p[4]) ; PIX_SORT(p[0],p[3]) ; + PIX_SORT(p[1],p[4]) ; PIX_SORT(p[1],p[2]) ; PIX_SORT(p[2],p[3]) ; + PIX_SORT(p[1],p[2]) ; return(*p[2]) ; +}float opt_med7(float **p, int n __attribute__((unused))){ + PIX_SORT(p[0], p[5]) ; PIX_SORT(p[0], p[3]) ; PIX_SORT(p[1], p[6]) ; + PIX_SORT(p[2], p[4]) ; PIX_SORT(p[0], p[1]) ; PIX_SORT(p[3], p[5]) ; + PIX_SORT(p[2], p[6]) ; PIX_SORT(p[2], p[3]) ; PIX_SORT(p[3], p[6]) ; + PIX_SORT(p[4], p[5]) ; PIX_SORT(p[1], p[4]) ; PIX_SORT(p[1], p[3]) ; + PIX_SORT(p[3], p[4]) ; return (*p[3]) ; +}float opt_med9(float **p, int n __attribute__((unused))){ + PIX_SORT(p[1], p[2]) ; PIX_SORT(p[4], p[5]) ; PIX_SORT(p[7], p[8]) ; + PIX_SORT(p[0], p[1]) ; PIX_SORT(p[3], p[4]) ; PIX_SORT(p[6], p[7]) ; + PIX_SORT(p[1], p[2]) ; PIX_SORT(p[4], p[5]) ; PIX_SORT(p[7], p[8]) ; + PIX_SORT(p[0], p[3]) ; PIX_SORT(p[5], p[8]) ; PIX_SORT(p[4], p[7]) ; + PIX_SORT(p[3], p[6]) ; PIX_SORT(p[1], p[4]) ; PIX_SORT(p[2], p[5]) ; + PIX_SORT(p[4], p[7]) ; PIX_SORT(p[4], p[2]) ; PIX_SORT(p[6], p[4]) ; + PIX_SORT(p[4], p[2]) ; return(*p[4]) ; +} float opt_med25(float **p, int n __attribute__((unused))){ + PIX_SORT(p[0], p[1]) ; PIX_SORT(p[3], p[4]) ; PIX_SORT(p[2], p[4]) ; + PIX_SORT(p[2], p[3]) ; PIX_SORT(p[6], p[7]) ; PIX_SORT(p[5], p[7]) ; + PIX_SORT(p[5], p[6]) ; PIX_SORT(p[9], p[10]) ; PIX_SORT(p[8], p[10]) ; + PIX_SORT(p[8], p[9]) ; PIX_SORT(p[12], p[13]); PIX_SORT(p[11], p[13]) ; + PIX_SORT(p[11], p[12]); PIX_SORT(p[15], p[16]); PIX_SORT(p[14], p[16]) ; + PIX_SORT(p[14], p[15]); PIX_SORT(p[18], p[19]); PIX_SORT(p[17], p[19]) ; + PIX_SORT(p[17], p[18]); PIX_SORT(p[21], p[22]); PIX_SORT(p[20], p[22]) ; + PIX_SORT(p[20], p[21]); PIX_SORT(p[23], p[24]); PIX_SORT(p[2], p[5]) ; + PIX_SORT(p[3], p[6]) ; PIX_SORT(p[0], p[6]) ; PIX_SORT(p[0], p[3]) ; + PIX_SORT(p[4], p[7]) ; PIX_SORT(p[1], p[7]) ; PIX_SORT(p[1], p[4]) ; + PIX_SORT(p[11], p[14]); PIX_SORT(p[8], p[14]) ; PIX_SORT(p[8], p[11]) ; + PIX_SORT(p[12], p[15]); PIX_SORT(p[9], p[15]) ; PIX_SORT(p[9], p[12]) ; + PIX_SORT(p[13], p[16]); PIX_SORT(p[10], p[16]); PIX_SORT(p[10], p[13]) ; + PIX_SORT(p[20], p[23]); PIX_SORT(p[17], p[23]); PIX_SORT(p[17], p[20]) ; + PIX_SORT(p[21], p[24]); PIX_SORT(p[18], p[24]); PIX_SORT(p[18], p[21]) ; + PIX_SORT(p[19], p[22]); PIX_SORT(p[8], p[17]) ; PIX_SORT(p[9], p[18]) ; + PIX_SORT(p[0], p[18]) ; PIX_SORT(p[0], p[9]) ; PIX_SORT(p[10], p[19]) ; + PIX_SORT(p[1], p[19]) ; PIX_SORT(p[1], p[10]) ; PIX_SORT(p[11], p[20]) ; + PIX_SORT(p[2], p[20]) ; PIX_SORT(p[2], p[11]) ; PIX_SORT(p[12], p[21]) ; + PIX_SORT(p[3], p[21]) ; PIX_SORT(p[3], p[12]) ; PIX_SORT(p[13], p[22]) ; + PIX_SORT(p[4], p[22]) ; PIX_SORT(p[4], p[13]) ; PIX_SORT(p[14], p[23]) ; + PIX_SORT(p[5], p[23]) ; PIX_SORT(p[5], p[14]) ; PIX_SORT(p[15], p[24]) ; + PIX_SORT(p[6], p[24]) ; PIX_SORT(p[6], p[15]) ; PIX_SORT(p[7], p[16]) ; + PIX_SORT(p[7], p[19]) ; PIX_SORT(p[13], p[21]); PIX_SORT(p[15], p[23]) ; + PIX_SORT(p[7], p[13]) ; PIX_SORT(p[7], p[15]) ; PIX_SORT(p[1], p[9]) ; + PIX_SORT(p[3], p[11]) ; PIX_SORT(p[5], p[17]) ; PIX_SORT(p[11], p[17]) ; + PIX_SORT(p[9], p[17]) ; PIX_SORT(p[4], p[10]) ; PIX_SORT(p[6], p[12]) ; + PIX_SORT(p[7], p[14]) ; PIX_SORT(p[4], p[6]) ; PIX_SORT(p[4], p[7]) ; + PIX_SORT(p[12], p[14]); PIX_SORT(p[10], p[14]); PIX_SORT(p[6], p[7]) ; + PIX_SORT(p[10], p[12]); PIX_SORT(p[6], p[10]) ; PIX_SORT(p[6], p[17]) ; + PIX_SORT(p[12], p[17]); PIX_SORT(p[7], p[17]) ; PIX_SORT(p[7], p[10]) ; + PIX_SORT(p[12], p[18]); PIX_SORT(p[7], p[12]) ; PIX_SORT(p[10], p[18]) ; + PIX_SORT(p[12], p[20]); PIX_SORT(p[10], p[20]); PIX_SORT(p[10], p[12]) ; + return (*p[12]); +} +float quick_select(float **arr, int n){ + int low, high; + int median; + int middle, ll, hh; + float ret; + low = 0 ; high = n-1 ; median = (low + high) / 2; + for(;;){ + if(high <= low) /* One element only */ + break; + if(high == low + 1){ /* Two elements only */ + PIX_SORT(arr[low], arr[high]) ; + break; + } + /* Find median of low, middle and high items; swap into position low */ + middle = (low + high) / 2; + PIX_SORT(arr[middle], arr[high]) ; + PIX_SORT(arr[low], arr[high]) ; + PIX_SORT(arr[middle], arr[low]) ; + /* Swap low item (now in position middle) into position (low+1) */ + ELEM_SWAP(arr[middle], arr[low+1]) ; + /* Nibble from each end towards middle, swapping items when stuck */ + ll = low + 1; + hh = high; + for(;;){ + do ll++; while (*arr[low] > *arr[ll]); + do hh--; while (*arr[hh] > *arr[low]); + if(hh < ll) break; + ELEM_SWAP(arr[ll], arr[hh]) ; + } + /* Swap middle item (in position low) back into correct position */ + ELEM_SWAP(arr[low], arr[hh]) ; + /* Re-set active partition */ + if (hh <= median) low = ll; + if (hh >= median) high = hh - 1; + } + ret = *arr[median]; + return ret; +} +#undef PIX_SORT +#undef ELEM_SWAP + +/* +#define _0(x) (x & 0x7FF) // lower 11 bits +#define _1(x) (x >> 11 & 0x7FF) // middle 11 bits +#define _2(x) (x >> 22 ) // upper 10 bits +float hist_select(float **arr, int n){ + inline uint32_t FloatFlip(uint32_t f){ + uint32_t mask = -((int32_t)(f >> 31)) | 0x80000000; + return f ^ mask; + } + inline uint32_t IFloatFlip(uint32_t f){ + uint32_t mask = ((f >> 31) - 1) | 0x80000000; + return f ^ mask; + } + uint32_t b0 +} +#undef _0 +#undef _1 +#undef _2 +*/ + +/* + * Median filtering + * Input: + * ima - picture (free() should be in a caller) + * f - filter + * sizex, sizey - picture size + * Output: + * result - filtered picture, memory allocates in this routine. + * Image borders that didn't pass through the filter (+- 1/2 of filter size) + * filled zeros + */ +int MedFilter(float *ima, + float **result, + Filter *f, + int sizex, + int sizey + ){ + int xlow = f->w / 2, xhigh = f->w - xlow;// filter area borders [x0-xlow, x0+xhigh) + int ylow = f->h / 2, yhigh = f->h - ylow; // [y0-ylow, y0+yhigh) + int x, xm=sizex-xhigh, ym=sizey-yhigh; // xm,ym - upper boundaries if filtered picture + int ssize=sizex*sizey, fsz=f->w*f->h, H = f->h - 1; + #ifdef EBUG + double t0 = dtime(); + #endif + float (*medfn)(float **p, int n); + *result = calloc(ssize, sizeof(float)); + if(!result) return FALSE; + switch(f->w*f->h){ + case 3: + medfn = opt_med3; + break; + case 5: + medfn = opt_med5; + break; + case 7: + medfn = opt_med7; + break; + case 9: + medfn = opt_med9; + break; + case 25: + medfn = opt_med25; + break; + default: + medfn = quick_select; + break; + } + /* + * Selection of picture elements on the square wxh into an array arr + * x0, y0 - coordinates of the center of the square + * cntr - current replacement string, or -1 if it is necessary to fill the entire arr + * If cntr!=-1 next line replaces a string cntr + */ + float selarr0(int x0, int y0, float *arr, float **sel){ + int x,y,tx,ty; + float *tmp, *ptr; + tx=x0+xhigh; ty=y0+yhigh; + tmp = arr; + for(y=y0-ylow; yw]; // pointer to a changed line + ptr = &ima[(ty-1)*sizex+x0-xlow]; + for(x=x0-xlow; x H) *cntr = 0; + return medfn(sel, fsz); + } + #pragma omp parallel + { + float *arr = calloc(fsz, sizeof(float*)); // array to sample storage + float **sel = calloc(fsz, sizeof(float*));// array to sorted sample storage + for(x = 0; x < fsz; x++) sel[x] = arr + x; + if(arr){ + #pragma omp for + for(x = xlow; x < xm; x++){ + int y, cntr = 0; + (*result)[ylow*sizex + x] = selarr0(x,ylow, arr, sel); + for(y = ylow+1; y < ym; y++){ + (*result)[y*sizex + x] = selarr1(x,y, arr, sel, &cntr); + } + } + free(arr); + free(sel); + } + } + DBG("time=%f\n", dtime()-t0); + return TRUE; +} +/* + * Fill isolines' scale (an array) + * Input: + * f - filter for given method + * min - minimum value of intensity + * wd - max-min (dinamic range) + * Output: + * scale - a pointer to array (allocated in this function) + */ +int fillIsoScale(Filter *f, float **scale, float min, float wd){ + int M = f->w, y; + float (*scalefn)(float in); + float step, Nsteps = (float)f->w; // amount of intervals + float Suniform(float in){ + return step*in + min; + } + float Slog(float in){ + return expf(in*step) + min - 1.; + } + float Sexp(float in){ + return wd*logf(in*step) + min; + } + float Ssqrt(float in){ + return in*in*step*step + min; + } + float Spow(float in){ + return sqrtf(in*step) + min; + } + if(!scale) return FALSE; + + *scale = calloc(M, sizeof(float)); + if(!*scale) return FALSE; + switch(f->h){ + case LOG: + scalefn = Slog; step = logf(wd+1.)/Nsteps; + break; + case EXP: + scalefn = Sexp; step = expf(1.)/Nsteps; + break; + case SQRT: + scalefn = Ssqrt; step = sqrtf(wd)/Nsteps; + break; + case POW: + scalefn = Spow; step = wd*wd/Nsteps; + break; + default: + scalefn = Suniform; step = wd/Nsteps; + } + for(y = 0; y < M; y++){ + (*scale)[y] = scalefn(y+1); + DBG("level %d: I=%g", y, (*scale)[y]); + } + return TRUE; +} + +/* + * Threshold filtering ("posterization") + * Input: + * ima - picture (free() must be executed in the caller) + * f - filter: + * f-> w - number of levels of posterization, [2.255] + * f-> h - type of posterization (0 - uniform) + * sizex, sizey - image size + * min, max - minimum and maximum intensity of the image + * Output: + * result - filtered image, the memory is allocated in this procedure + * scale - the scale of intensities, the memory is allocated here (if the scale!=NULL) + * + * TODO: save the result in the char, not float; learn display function + */ +int StepFilter(float *ima, float **result, Filter *f, int sizex, int sizey, + float min, float max, float **scale){ + if(f->w < 2 || f->w > 255) return FALSE; + int y; + float Nsteps = (float)f->w; // amount of intervals + float step; + float wd = max - min; + if(fabs(wd) < FLT_EPSILON) return FALSE; + float (*stepfn)(float in); + float Funiform(float in){ + return floor((in-min)/step); + } + float Flog(float in){ + return floor(logf(in-min+1.)/step); + } + float Fexp(float in){ + return floor(expf((in-min)/wd)/step); + } + float Fsqrt(float in){ + return floor(sqrtf(in-min)/step); + } + float Fpow(float in){ + return floor((in-min)*(in-min)/step); + } + #ifdef EBUG + double t0 = dtime(); + #endif + switch(f->h){ + case LOG: + stepfn = Flog; + step = logf(max-min+1.)/Nsteps; + break; + case EXP: + stepfn = Fexp; + step = expf(1.)/Nsteps; + break; + case SQRT: + stepfn = Fsqrt; + step = sqrtf(wd)/Nsteps; + break; + case POW: + stepfn = Fpow; + step = wd*wd/Nsteps; + break; + default: + stepfn = Funiform; + step = wd/Nsteps; + } + *result = calloc(sizex*sizey, sizeof(float)); + if(!result) return FALSE; + #pragma omp parallel for + for(y = 0; y < sizey; y++){ + int x; float *out, *in; + in = ima + y*sizex; out = *result + y*sizex; + for(x = 0; x < sizex; x++, in++, out++){ + *out = stepfn(*in); + } + } + if(scale && !fillIsoScale(f, scale, min, wd)) g_err(_("No memory left")); + DBG("SF: %d sublevels, step=%g, time=%f\n", f->w, step, dtime()-t0); + return TRUE; +} + + + + diff --git a/src/contours.c b/src/contours.c new file mode 100644 index 0000000..ccb88e1 --- /dev/null +++ b/src/contours.c @@ -0,0 +1,506 @@ +// contours.c - find isophotos +// +// Copyright 2011 Edward V. Emelianoff +// +// This program 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 2 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 this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +// MA 02110-1301, USA. + +#include "contours.h" +#include "opengl.h" + +// contours' minimum size limits +const int MIN_CONTOUR_SIZE = 4; +const int MIN_CLOSED_CONTOUR_SIZE = 7; +// maximum amount of contour levels +const int MAX_CONTOUR_LEVELS = 255; + +// frees contour's data +void free_contour(Contour **c){ + cPoint *pp, *p; + p = (*c)->first; + while(p){ // go through all points of current contour + pp = p; p = p->next; + _FREE(pp); + } + _FREE(*c); +} + +// delete contours' list for the picture +void free_contours(IMAGE *image){ + FNAME(); + int i; + cList *l; + Contour *cp, *c; + if(image->Ncontours < 1 || !image->contours) return; + for(i = 0; i < image->Ncontours; i++){ // go through all levels + l = image->contours[i]; + c = l->first; + while(c){ // go throug all contours of current level + cp = c; c = c->next; + free_contour(&cp); + } + } +} + +// create new element of contours' list +cList *new_clist(int lvl){ + DBG("level: %d", lvl); + cList *c = calloc(1, sizeof(cList)); + c->L = lvl; + return c; +} +// create new contour +Contour *new_contour(){ + Contour *c = calloc(1, sizeof(Contour)); + return c; +} +// create new point +cPoint *new_point(){ + cPoint *p = calloc(1, sizeof(cPoint)); + return p; +} +// add contour c to list +void cList_add(cList *cl, Contour *c){ + cl->N++; + //DBG("contour #%d added to list with levl %d", cl->N, cl->L); + if(!cl->first) + cl->first = c; + else + cl->last->next = c; + cl->last = c; +} +// add point p to contour c +// headflag == 0 - add to tail; ==1 - add to head +void c_add(Contour *c, cPoint *p, char headflag){ + c->N++; + if(!c->first || !c->last){ + c->first = p; + c->last = p; + }else{ + if(headflag){ // add point to head + c->first->prev = p; + p->next = c->first; + c->first = p; + } + else{ + c->last->next = p; // add point to tail + p->prev = c->last; + c->last = p; + } + } +} + +/* + * copy contours from image in to image out + * with shift (dX, dY): Xnew=Xold+dX, Ynew=Yold+dY + */ +int copy_contours(IMAGE *in, IMAGE *out, float dX, float dY){ + int i, N = in->Ncontours; + cList *l; + cPoint *pCur, *p; + Contour *cCur, *c; + if(N < 1 || !N) return TRUE; + if(!(out->contours = calloc(in->Ncontours, sizeof(Contour)))) return FALSE; + for(i = 0; i < N; i++){ // go through all levels + l = in->contours[i]; + if(!(out->contours[i] = new_clist(i))) goto badExit; + c = l->first; + while(c){ // go throug all contours of current level + if(!(cCur = new_contour())) goto badExit; + pCur = c->first; + while(pCur){ + if(!(p = new_point())) goto badExit; + p->x = pCur->x+dX; p->y = pCur->y+dY; + c_add(cCur, p, 0); + pCur = pCur->next; + } + cList_add(out->contours[i], cCur); + c = c->next; + } + } + out->Ncontours = in->Ncontours; + return TRUE; +badExit: + free_contours(out); return FALSE; +} + +// direction bits +enum{ + D_RIGHT = 1 + ,D_LEFT = 2 + ,D_DOWN = 4 + ,D_UP = 8 +}; + +// directions of next moving for mask values +// spetial values 6 & 9 also checks separately +int directions[16] = { + 0, // no isoline + D_RIGHT | D_DOWN, + D_LEFT | D_DOWN, + D_RIGHT | D_LEFT, + D_RIGHT | D_UP, + D_UP | D_DOWN, + 0, // special point 6 + D_LEFT | D_UP, + D_LEFT | D_UP, + 0, // special point 9 + D_UP | D_DOWN, + D_RIGHT | D_UP, + D_RIGHT | D_LEFT, + D_LEFT | D_DOWN, + D_RIGHT | D_DOWN, + 0 // no isoline +}; + +/* + * coordinate shifts {dx, dy} according to direction value bits + * their values are choosen so, that even if there would be some non-zero + * bits in direction mask, new direction of isoline search will be choosen + * as possible nearest to "right" or " down" direction + */ +int dxdy[9][2] = { + {0 , 0}, // 0 + {1 , 0}, // D_RIGHT + {-1, 0}, // D_LEFT + {0 , 0}, + {0 , 1}, // D_DOWN + {0 , 0}, + {0 , 0}, + {0 , 0}, + {0 ,-1} // D_UP +}; +// new directions in simplex case, 2nd dimension - head flag (if==1, rotate CCW) +int newdirs[16][2] = { + {0, 0}, // 0 + {D_RIGHT,D_RIGHT},// D_RIGHT + {D_LEFT, D_LEFT}, // D_LEFT + {D_RIGHT, D_LEFT},// D_RIGHT | D_LEFT + {D_DOWN, D_DOWN}, // D_DOWN + {D_RIGHT, D_DOWN},// D_DOWN | D_RIGHT + {D_DOWN, D_LEFT}, // D_DOWN | D_LEFT + {D_RIGHT, D_LEFT},// D_DOWN | D_RIGHT | D_LEFT + {D_UP, D_UP}, // D_UP + {D_RIGHT, D_UP}, // D_UP | D_RIGHT + {D_LEFT, D_UP}, // D_UP | D_LEFT + {D_RIGHT, D_UP}, // D_UP | D_RIGHT | D_LEFT + {D_DOWN, D_UP}, // D_UP | D_DOWN + {D_RIGHT, D_UP}, // D_UP | D_DOWN | D_RIGHT + {D_DOWN, D_UP}, // D_UP | D_DOWN | D_LEFT + {D_RIGHT, D_UP} // D_UP | D_DOWN | D_RIGHT | D_LEFT +}; + +/* + * Functions that calculate the position of the contour (linear interpolation) + * P - the point at which are recorded the coordinates (relative to the center of the upper left quadrant) + * to the resulting coordinates p shold be added the coordinates of the square to obtain the coordinates in image SC + * A, b, c, d - intensity in the UL, UR DL and DR corners of the square + * Lvl - contours level + */ +// right & up borders +void getRU(cPoint *p, float a, float b, float c __attribute__((unused)), float d, float I){ + float x = 0., y = 0.; + if(fabs(b-a) > FLT_EPSILON) x = (I-a)/(b-a); + if(fabs(d-b) > FLT_EPSILON) y = (I-b)/(d-b); + p->x = x/2.+0.5; p->y = y/2.; +} +// left & up +void getLU(cPoint *p, float a, float b, float c, float d __attribute__((unused)), float I){ + float x = 0., y = 0.; + if(fabs(b-a) > FLT_EPSILON) x = (I-a)/(b-a); + if(fabs(c-a) > FLT_EPSILON) y = (I-a)/(c-a); + p->x = x/2.; p->y = y/2.; +} +// left & down +void getLD(cPoint *p, float a, float b __attribute__((unused)), float c, float d, float I){ + float x = 0., y = 0.; + if(fabs(d-c) > FLT_EPSILON) x = (I-c)/(d-c); + if(fabs(c-a) > FLT_EPSILON) y = (I-a)/(c-a); + p->x = x/2.; p->y = y/2.+0.5; +} +// right & down +void getRD(cPoint *p, float a __attribute__((unused)), float b, float c, float d, float I){ + float x = 0., y = 0.; + if(fabs(d-c) > FLT_EPSILON) x = (I-c)/(d-c); + if(fabs(d-b) > FLT_EPSILON) y = (I-b)/(d-b); + p->x = x/2.+0.5; p->y = y/2.+0.5; +} +// up & down +void getUD(cPoint *p, float a, float b, float c, float d, float I){ + float x1 = 0., x2 = 1.; + if(fabs(d-c) > FLT_EPSILON) x1 = (I-c)/(d-c); + if(fabs(b-a) > FLT_EPSILON) x2 = (I-a)/(b-a); + p->x = (x1+x2)/2.; p->y = 0.5; +} +// right & left +void getRL(cPoint *p, float a, float b, float c, float d, float I){ + float y1 = 0., y2 = 1.; + if(fabs(d-b) > FLT_EPSILON) y1 = (I-b)/(d-b); + if(fabs(c-a) > FLT_EPSILON) y2 = (I-a)/(c-a); + p->x = 0.5; p->y = (y1+y2)/2.; +} +// bung for a case +void getNull(cPoint *p __attribute__((unused)), float a __attribute__((unused)), + float b __attribute__((unused)), float c __attribute__((unused)), + float d __attribute__((unused)), float I __attribute__((unused))){ + p->x = 0.; p->y = 0.; +} + +// array of a funtions to coordinates calculation +typedef void (*FnXY)(cPoint *p, float a, float b, float c, float d, float I); +FnXY getXY[16] = { + getNull, // 0 + getRD, + getLD, + getRL, + getRU, + getUD, + getNull,//6 must be getLU or get RD + getLU, + getLU, + getNull,//9 must be getLD or get RU + getUD, + getRU, + getRL,//12 + getLD, + getRD, + getNull +}; + +static float *isoscale = NULL; +static int w, w1, h, h1; +static cList **contours = NULL; + +/* + * filling of the contour line, beginning at the point x1, y1 + * flag==1 - add points not to the tail of the circuit but to the head + * cCur - contour to add the point + * imdata - the original picture + * lvl - isoline level number + * olddir - direction (D_...) to a previous point + * mask - pointer to current mask + */ +int proc_contour(int x1, int y1, int flag, + Contour *cCur, float *imdata, + int lvl, int olddir, + unsigned char *mask){ + int frst = flag; + float Lvl = isoscale[lvl]; // intensity at isoline level lvl + float *point; + int newdir; + do{ + int mid = y1*w1 + x1, iid = y1*w + x1; + unsigned char m = mask[mid]; + if(m == 0 || m > 14) break; + if(!frst){ + cPoint *p = new_point(); + if(!p) return FALSE; + c_add(cCur, p, flag); + // get coordinates p->x, p->y (by simplified linear approximation) + point = imdata + iid; // LU pixel for a square + // find coordinates + switch(m){ // check special points 6 & 9, their values depends on olddir + case 6: // m = 7/14, mask=14/7 + switch(olddir){ + case D_LEFT: // next - UP or LEFT + case D_UP: + m = 7; mask[mid] = 14; // clear only used point + break; + default: // D_DOWN/D_RIGHT, next - RIGHT or UP + m = 14; mask[mid] = 7; + } + break; + case 9: // m = 13/11, mask=11/13 + switch(olddir){ + case D_LEFT: // next - DOWN or LEFT + case D_DOWN: + m = 13; mask[mid] = 11; // clear only used point + break; + default: // D_UP/D_RIGHT, next - RIGHT or DOWN + m = 11; mask[mid] = 13; + } + break; + default: + mask[mid] = 0; // just clear used mask's point + } + getXY[m](p,point[0],point[1],point[w],point[w+1],Lvl); + if(p->x < -0.5 || p->x > 1.5) p->x = 0.5; + if(p->y < -0.5 || p->y > 1.5) p->y = 0.5; + // add .5, beacause of mask's shift + p->x += (float)x1 + .5; + p->y += (float)y1 + .5; + } + gboolean found = FALSE; // whether next isoline point was found + int x2, y2; + do{ // find next contour's point + unsigned char pp; + // check right new direction + newdir = newdirs[directions[m] & (~olddir)][flag]; + //newdir = directions[m] & (~olddir); + if(newdir > 8){DBG("WTF? m=%d, newdir=%d, olddir=%d",m,newdir, olddir); break;} // This can't be so, but WTF if? + x2 = x1 + dxdy[newdir][0]; + y2 = y1 + dxdy[newdir][1]; + if(x1 == x2 && y1 == y2) break; + // is next square out of an picture? + if(x2 < 0 || x2 >= w1 || y2 < 0 || y2 >= h1) break; + int ii = y2*w1+x2; + pp = mask[ii]; + // has the next square a contour points? + if(pp == 0 || pp > 14) break; + found = TRUE; + }while(0); + x1 = x2; y1 = y2; + if(!found){ + break; // end of isoline + } + switch(newdir){ // calculate old direction + case D_LEFT: olddir = D_RIGHT; break; + case D_RIGHT: olddir = D_LEFT; break; + case D_UP: olddir = D_DOWN; break; + case D_DOWN: olddir = D_UP; break; + default: olddir = 0; + } + frst = FALSE; + }while(1); // cycle through the contour + return TRUE; +} +/* + * Processing of the point with coordinates x, y + * imdata - the original picture + * lvl - number of contour's level + * mask - current mask + * return FALSE if failed + */ +int process_it_(int x, int y, int lvl, float *imdata, unsigned char *mask){ + Contour *cCur; + unsigned char pt0 = mask[y*w1+x]; + if(pt0 == 0 || pt0 > 14) return TRUE;; + do{ + if(!contours[lvl]){ // countour wasn't created - create it + contours[lvl] = new_clist(lvl); + if(!contours[lvl]){ + g_err(_("Can't allocate memory")); + return FALSE; + } + } + cCur = new_contour(); + if(!cCur){ + g_err(_("Can't allocate memory")); + return FALSE; + } + // start processing contour, adding points to its tail + // D_LEFT - beacause we check points from left to the right + if(!proc_contour(x, y, 0, cCur, imdata, lvl, D_LEFT, mask)) + return FALSE; + // continue processing contour, adding points to its head + // D_UP - beacause there's no more contour points upper + // if((y + 1) < w1) // mask(x,y) == 0, so we check down point + // if(!proc_contour(x, y+1, 1, cCur, imdata, lvl, D_UP, mask)) + // return FALSE; + if(cCur->N < MIN_CONTOUR_SIZE){ // contour is too short + free_contour(&cCur); + break; + } + if( (fabs(cCur->first->x - cCur->last->x) + + fabs(cCur->first->y - cCur->last->y)) < 2.){ // closed contour + cCur->closed = TRUE; + if(cCur->N < MIN_CLOSED_CONTOUR_SIZE){ // contour is too short + free_contour(&cCur); + break; + } + } + cList_add(contours[lvl], cCur); + }while(0); + return TRUE; +} + + +/* + * Find isophotos on picture window->image + * nLevl - amount of isophotos + * type - type of isophotos segmentation (the same as f->h for STEP filter) + */ +void find_contours(Window *window, + int nLevl, + int type){ + IMAGE *ima = window->image; + int y, level; + float *image = ima->data; + if(nLevl < 2 || nLevl > MAX_CONTOUR_LEVELS) return; + unsigned char *mask; // array for marching squares matrix + Filter f; + #ifdef EBUG + double t0 = dtime(); + #endif + w = ima->width; w1=w-1; h = ima->height; h1=h-1; + mask = calloc((w1)*(h1),1); // mask - one mask pixel is square of 2x2 pixels on an picture + if(!mask){g_err(_("Can't allocate memory"));return;} + // prepare isoscale + f.FilterType = STEP; f.w = nLevl; f.h = type; + if(!fillIsoScale(&f, &isoscale, ima->stat.min, ima->stat.max - ima->stat.min)){ + g_err(_("Can't allocate memory"));goto endOF;} + // Go through all levels + contours = calloc(nLevl, sizeof(Contour)); // all-picture contours array + if(!contours){g_err(_("Can't allocate memory"));goto endOF;} + for(level = 0; level < nLevl; ++level){ + /* + * 1. level segmentation & build mask + * Mask by square (if zero point in left upper corner) + * _______ + * | a | b | + * |---+---| + * | c | d | + * ------- + * bits order: 0000abcd + */ + int y; + float lvl = isoscale[level]; // current isoline's intensity level + #pragma omp parallel for + for(y = 0; y < h1; y++){ + int x; + unsigned char *out = &mask[y*w1]; + float *in = &image[y*w]; + for(x = 0; x < w1; x++, in++, out++) + *out = (unsigned char) + ((in[0]>lvl)<<3) | + ((in[1]>lvl)<<2) | + ((in[w]>lvl)<<1) | + (in[w+1]>lvl); + } + // go through all mask's points + for(y = 0; y < h1; y++){ + int x; + for(x = 0; x < w1; x++) + if(!process_it_(x, y, level, image, mask)) goto endOF; + } + } +endOF: + _FREE(mask); + if(contours){ + for(y = 0; y < nLevl; y++){ + g_print("Level: %d", y); + if(contours[y]) g_print(", %d contours\n", contours[y]->N); + else g_print("\n"); + } + // fill contour structure + ima->contours = contours; contours = NULL; + ima->Ncontours = nLevl; + if(isoscale){ima->cLevels = isoscale; isoscale = NULL;} + } + DBG("time: %g", dtime()-t0); + window->context->visualMode |= SHOW_ISOLINES; + gen_texture(ima, window, TRUE); + force_redraw(window->drawingArea); +} diff --git a/src/filelist.c b/src/filelist.c new file mode 100644 index 0000000..eb64438 --- /dev/null +++ b/src/filelist.c @@ -0,0 +1,168 @@ +// filelist.c - functions to work with list of FITS files in current directory +// +// Copyright 2011 Edward V. Emelianoff +// +// This program 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 2 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 this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +// MA 02110-1301, USA. + +#include "filelist.h" +#include "fits.h" +/* + * receiving the file name of list, type can be one of the following: + * CURRENT - current file + * FIRST - first file + * LAST - last file + * PREVIOUS - previous file + * NEXT - next file + */ +gchar *get_filename(guchar type, Window *window){ + DBG("length of list = %d", window->files.list_length); + if(window->files.list_length < 1) return NULL; + if(!window->files.list_current){ + DBG("No current file. WTF?"); + return NULL; + } + //~ g_list_foreach(window->files.files_list, (GFunc)prnt, NULL); + switch(type){ + case FIRST: + if(window->files.list_current == window->files.files_list){ + DBG("1st or only file"); + return NULL; // try to open current opened file or there's no files + } + window->files.list_current = window->files.files_list; + break; + case LAST: + if(window->files.list_length < 2 || + window->files.list_current == window->files.list_end){ + DBG("last or only file"); + return NULL; + } + window->files.list_current = window->files.list_end; + break; + case PREVIOUS: + if(!window->files.list_current->prev){ + DBG("1st file"); + return NULL; + } + window->files.list_current = window->files.list_current->prev; + break; + case NEXT: + if(!window->files.list_current->next){ + DBG("last file"); + return NULL; + } + window->files.list_current = window->files.list_current->next; + break; + default: // current + break; + } + if(!window->files.list_current || !window->files.list_current->data){ + DBG("no current file? WTF?"); + return NULL; + } + DBG("change filename: %s\n\n", (gchar*)window->files.list_current->data); + return (gchar*)window->files.list_current->data; +} + +// get filename suffix +gboolean get_ext(gchar *filename){ + gchar *basename, *ext; + basename = strrchr(filename, '/'); + if(!basename) basename = filename; + else basename++; + ext = strrchr(basename, '.'); + if(!ext) return FALSE; + else ext++; + //DBG("basename = %s; ext = %s", basename, ext); + if(strcasecmp(ext, "fts") == 0 || strcasecmp(ext, "fit") == 0 || + strcasecmp(ext, "fits") == 0) return TRUE; + return FALSE; +} + +// File test for loadable +gboolean is_loadable(gchar *filename){ + if(!Global->add_all){ // if we don't try to open any file + if(!get_ext(filename)) // file suffix isn't fit/fits/fts + return FALSE; + } + return guess_fits(filename); +} + +gint add_file_to_list(gchar *filename, Window *window){ + if(is_loadable(filename)){ + DBG("File %s is FITS file\n", filename); + window->files.files_list = g_list_prepend(window->files.files_list, (gpointer)filename); + window->files.list_length++; + if(window->files.list_length == 1) + window->files.list_end = window->files.files_list; + return 1; + } + return 0; +} + +void free_filelist(Window *window){ + if(window->files.files_list){ + g_list_foreach(window->files.files_list, (GFunc)free, NULL); + g_list_free(window->files.files_list); + window->files.list_length = 0; + window->files.files_list = NULL; + } +} + +/* + * get a FITS files list in current directory + * filename - full filename (with path) + * returns a files number in list + */ +gint fill_filelist(gchar *filename, Window *window){ + gint nb_inserted = 0; + GDir *dir; + GError *err = NULL; + const gchar *dir_entry; + gchar *dirname = g_path_get_dirname(filename); + DBG("Open directory %s", dirname); + dir = g_dir_open(dirname, 0, &err); + if(dir == NULL){ + g_err(err->message); + g_error_free(err); + return 0; + } + free_filelist(window); // clear list if it was already created + while((dir_entry = g_dir_read_name(dir))){ + gchar *full_path = g_build_filename(dirname, dir_entry, NULL); + if(!g_file_test(full_path, G_FILE_TEST_IS_DIR)){ + nb_inserted += add_file_to_list(full_path, window); + } + } + g_dir_close(dir); + g_free(dirname); + window->files.files_list = g_list_sort(window->files.files_list, (GCompareFunc)strcmp); + DBG("\nSort %d items\n", nb_inserted); + window->files.list_end = g_list_last(window->files.files_list); + window->files.list_current = g_list_find_custom(window->files.files_list, filename,(GCompareFunc)strcmp); +//if(!window->files.list_current) window->files.list_current = g_list_first(window->files.files_list); + //~ place_first = g_list_find_custom(window->files.files_list, filename, strcmp); + //~ if(place_first && place_first->prev){ + //~ place_first->prev->next = place_first->next; + //~ if(place_first->next) + //~ place_first->next->prev = place_first->prev; + //~ place_first->prev = NULL; + //~ place_first->next = window->files.files_list; + //~ window->files.files_list->prev = place_first; + //~ window->files.files_list = place_first; + //~ } + return nb_inserted; +} + diff --git a/src/fits.c b/src/fits.c new file mode 100644 index 0000000..ab75c51 --- /dev/null +++ b/src/fits.c @@ -0,0 +1,396 @@ +// fits.c - main functions to work with FITS files +// +// Copyright 2011 Edward V. Emelianoff +// +// This program 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 2 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 this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +// MA 02110-1301, USA. +#include "fits.h" +#include "gtk.h" +#include "fitsheaders.h" + +/* + * Macros for error processing when working with cfitsio functions + */ +#define TRYFITS(f, ...) \ +do{ gboolean status = FALSE; \ + f(__VA_ARGS__, &status); \ + if(status){ \ + fits_report_error(stderr, status); \ + return FALSE;} \ +}while(0) +#define FITSFUN(f, ...) \ +do{ gboolean status = FALSE; \ + int ret = f(__VA_ARGS__, &status); \ + if(ret || status) \ + fits_report_error(stderr, status); \ +}while(0) +#define WRITEKEY(...) \ +do{ gboolean status = FALSE; \ + fits_write_key(__VA_ARGS__, &status); \ + if(status) fits_report_error(stderr, status);\ +}while(0) + +/* + * Check whether a number is a complex + * If yes - returns true, no - false + * If re and/or im!=NULL, in them stored values + * of real and imaginary components + */ +gboolean cmplx_conv(gchar *value, double *re, double *im){ + double sign = 1.; + double r,i; + gchar *ptr1, *ptr2, *eptr; + ptr1 = g_strstr_len(value, -1, "("); + if(ptr1){ // form is (re, im) or (re im) + ptr1++; + ptr2 = g_strstr_len(ptr1, -1, ")"); + if(ptr2) *ptr2 = 0; + ptr2 = g_strstr_len(ptr1, -1, ","); + if(!ptr2) + ptr2 = g_strstr_len(ptr1, -1, " "); + } + else{ // form is re + iim or re + jim + ptr1 = value; + ptr2 = g_strstr_len(ptr1, -1, "i"); + if(!ptr2) + ptr2 = g_strstr_len(ptr1, -1, "j"); + } + if(ptr2){ + *ptr2 = 0; + ptr2++; + } + else + return FALSE; + if(g_strrstr_len(ptr1, 3, "-")) sign=-1.; + r = strtod(ptr1, &eptr); + if(eptr == ptr1) return FALSE; + i = sign*strtod(ptr2, &eptr); + if(eptr == ptr2) return FALSE; + if(re) *re = r; + if(im) *im = i; + return TRUE; +} +/* + * try to guess key type: + * TLONGLONG, TDOUBLE, TDBLCOMPLEX or TSTRING + */ +int guess_type(gchar *keyval, gchar* keytype){ + char *eptr, *val; + int dtype = TSTRING; + val = strdup(keyval); + if(strchr(val, '.')){ + errno = 0; + strtod(val, &eptr); + if(eptr != val && errno != ERANGE) + dtype = TDOUBLE; + } + if(dtype == TSTRING){ + errno = 0; + strtoll(val, &eptr, 0); + if(eptr != val && errno != ERANGE) dtype = TLONGLONG; + } + if(dtype == TSTRING && cmplx_conv(val, NULL, NULL)) + dtype = TDBLCOMPLEX; + if(keytype) switch(dtype){ + case TDOUBLE: + sprintf(keytype, "TDOUBLE"); + break; + case TLONGLONG: + sprintf(keytype, "TLONGLONG"); + break; + case TDBLCOMPLEX: + sprintf(keytype, "TDBLCOMPLEX"); + break; + default: + sprintf(keytype, "TSTRING"); + } + //~ DBG("dtype: %d, keytype: %s", dtype, (keytype) ? keytype : ""); + free(val); + return dtype; +} +/* + * Save image-structure to a FITS file + * (together whith keys) + */ +gboolean writefits(char *filename, IMAGE *image){ + FNAME(); + long naxes[2] = {image->width, image->height}; + int w = image->width, h = image->height; + ssize_t sz = w * h; + gchar *keytype, *keyname, *keyval, *keycomment; int keyfitstype; + GtkTreeIter iter; + fitsfile *fp; + gchar *card; + #ifdef EBUG + int n = 0; + #endif + TRYFITS(fits_create_file, &fp, filename); + TRYFITS(fits_create_img, fp, image->dtype, 2, naxes); + if(image->store){ + GtkTreePath *path = gtk_tree_path_new_first(); + GtkTreeModel *tree_model = GTK_TREE_MODEL(image->store); + if(gtk_tree_model_get_iter(tree_model, &iter, path)){ // there's keys + DBG("filename: %s", filename); + do{ + gtk_tree_model_get(tree_model, &iter, + L_TYPENM, &keytype, + L_KEY, &keyname, + L_VAL, &keyval, + L_COMM, &keycomment, + L_TYPE, &keyfitstype, + -1); + if(keyfitstype == TSTRING) // make sure, that string is in quotes + if(*keyval != '\''){ + gchar *str = g_strdup_printf("'%s'", keyval); + g_free(keyval); + keyval = str; + } + card = g_strdup_printf("%-8s= %s / %s", + keyname, keyval, keycomment); + g_free(keytype); g_free(keyname); g_free(keyval); + g_free(keycomment); + DBG("key#%d: %s", ++n, card); + FITSFUN(fits_write_record, fp, card); + g_free(card); + }while(gtk_tree_model_iter_next(tree_model, &iter)); + } + gtk_tree_path_free(path); + } + GLfloat min, wd; + min = image->stat.min; + wd = image->stat.max - min; + DBG("min = %f, wd = %f", min, wd); +/* int i,j; + GLfloat *newdata = malloc(sz*sizeof(GLfloat)); + GLfloat *ptro = newdata, *ptri = image->data; + for(i=0; idata); + TRYFITS(fits_close_file, fp); + return TRUE; +} + +gboolean try_open_file(gchar *filename, IMAGE *ima){ + if(!readfits(filename, ima)){ + g_err(_("Can't read fits file")); + ima->data = NULL; + return FALSE; + } + return TRUE; +} +/* +char *del_spaces(char *str){ + char *beg, *end; + beg = str; + end = str + strlen(str); + while(*beg == ' ' && beg < end) beg++; + if(beg == end) return beg; + end--; + if(beg == end) return beg; + while(*end == ' ' && end > beg) + *end-- = 0; + return beg; +} +gboolean strip_card(char *card, char *keyname, char *keyval, char *keycomment){ + char *ptr, *ptr1, *end; + char *c = strdup(card); + end = c + strlen(c); + ptr = strchr(c, '='); + if(!ptr) return FALSE; + *ptr = 0; ptr++; + ptr1 = del_spaces(c); + strncpy(keyname, ptr1, FLEN_KEYWORD-1); + ptr1 = strchr(ptr, '\''); + if(ptr1){ + ptr1++; + ptr = strchr(ptr1, '\''); + if(!ptr){ + ptr = strchr(ptr1, '/'); + if(!ptr){ + free(c); + return FALSE; + } + *ptr = 0; ptr++; + }else{ + *ptr = 0; ptr = strchr(ptr+1, '/'); + if(!ptr){ + free(c); + return FALSE; + } + *ptr = 0; ptr++; + } + }else{ + ptr1 = ptr; + ptr = strchr(ptr1, '/'); + if(!ptr){ + free(c); + return FALSE; + } + *ptr = 0; ptr++; + ptr1 = del_spaces(ptr1); + } + if(ptr >= end) return FALSE; + ptr1 = del_spaces(ptr1); + strncpy(keyval, ptr1, FLEN_VALUE); + ptr1 = del_spaces(ptr); + strncpy(keycomment, ptr1, FLEN_COMMENT); + free(c); + return TRUE; +}*/ + +/* + * Read FITS file filename to a structure image + */ +gboolean readfits(gchar *filename, IMAGE *image){ + FNAME(); + gboolean ret = TRUE; + fitsfile *fp; + GLfloat nullval = 0., imBits, bZero = 0., bScale = 1.; + free(image->data); + image->data = NULL; + int i, j, hdunum, keynum, morekeys, dtype, stat; + int naxis, w, h; + double val_f = -1e6; // VAL_F value + long naxes[4]; + ssize_t sz; + //char card[FLEN_CARD]; + char keyname[FLEN_KEYWORD], keyval[FLEN_VALUE]; + char keycomment[FLEN_COMMENT], cdtype[32]; + TRYFITS(fits_open_file, &fp, filename, READWRITE); +/* TRYFITS(fits_get_hdrspace, fp, &keynum, &morekeys); + if(morekeys == -1){ + return FALSE; + } + g_print("Read header of %s, %d keys\n", filename, keynum);*/ + FITSFUN(fits_get_num_hdus, fp, &hdunum); + //~ g_print(_("\nFile includes %d HDUs\n"), hdunum); + if(hdunum < 1){ + g_err(_("Can't read HDU")); + TRYFITS(fits_close_file, fp); + return FALSE; + } + FITSFUN(fits_get_hdu_type, fp, &dtype); + //~ if(dtype) g_print(_("Current HDU type: %d\n"), dtype); + init_keylist(image); + FITSFUN(fits_get_hdrpos ,fp, &keynum, &morekeys); + + for (j = 1; j <= keynum; j++){ + /*FITSFUN(fits_read_record, fp, j, card); + if(!strip_card(card, keyname, keyval, keycomment)){ + g_print("Corrupted card: %s!\n", card); + continue; + }*/ + FITSFUN(fits_read_keyn, fp, j, keyname, keyval, keycomment); + if(strcmp(keyname, "SIMPLE") == 0 || strcmp(keyname, "EXTEND") == 0) // key "file does conform ..." + continue; + else if(strcmp(keyname, "COMMENT") == 0) // comment of obligatory key in FITS head + continue; + else if(strstr(keyname, "NAXIS") == keyname || strcmp(keyname, "BITPIX") == 0) // NAXIS, NAXISxxx, BITPIX + continue; + else if(strcmp(keyname, "BZERO") == 0){ + bZero = atof(keyval); + //continue; + } + else if(strcmp(keyname, "BSCALE") == 0){ + bScale = atof(keyval); + if(bScale < SCALE_MIN) bScale = 1.; + //continue; + } + else if(strcmp(keyname, "VAL_F") == 0){ + val_f = atof(keyval); + } + dtype = guess_type(keyval, cdtype); + //DBG("key#%d; name=%s, dtype=%d (%s), value=%s, comment=%s\n", + // j, keyname, dtype, cdtype, keyval, keycomment); + add_key(image, cdtype, keyname, keyval, keycomment, dtype); + } + if(hdunum > 1){ + for(i = 2; !(fits_movabs_hdu(fp, i, &dtype, &stat)); i++){ + FITSFUN(fits_get_hdrpos ,fp, &keynum, &morekeys); + for (j = 1; j <= keynum; j++){ + FITSFUN(fits_read_keyn, fp, j, keyname, keyval, keycomment); + dtype = guess_type(keyval, cdtype); + add_key(image, cdtype, keyname, keyval, keycomment, dtype); + } + } + if(stat != END_OF_FILE){ + fits_report_error(stderr, (stat)); + ret = FALSE; + } + } + + TRYFITS(fits_get_img_param, fp, 4, &dtype, &naxis, naxes); + if(dtype > 0) imBits = (GLfloat)(1LL << dtype); // compute the max amplitude + else imBits = -1.; // image has floating point type + DBG("Image type: %d (%f bits)", dtype, imBits); + image->maxVal = imBits; + image->dtype = dtype; + if(naxis != 2){ + g_err(_("Not an image? (dimensions != 2)")); + return FALSE;} + + image->val_f = val_f; + image->bZero = bZero; + image->bScale = bScale; + image->width = w = naxes[0]; + image->height = h = naxes[1]; + sz = (ssize_t)(w) * (ssize_t)(h); + image->data = malloc(sz * sizeof(GLfloat)); + + TRYFITS(fits_read_img, fp, TFLOAT, 1, sz, &nullval, image->data, &stat); + GLfloat *ptr = image->data; + GLfloat min, max, wd; + min = 1e6; max = 0.; + for(i=0; i max) max = tmp; + else if(tmp < min) min = tmp; + } + wd = max - min; + DBG("min = %f, wd = %f", min, wd); + image->stat.max = max; + image->stat.min = min; + // histogram isn't initialized yet + image->stat.histogram.data = NULL; + image->stat.histogram.size = 0; + /* + ptr = image->data; + for(i=0; i +// +// This program 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 2 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 this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +// MA 02110-1301, USA. +#include "fitsheaders.h" +#include "fits.h" + +GtkListStore *combo_ = NULL; +GtkTreeSelection *sel; +GtkTreeView* Tree; +GtkWidget *del_button; +GtkWidget *window; +// Data types to write FITS keys into menu +gchar* key_types[] = { + "TSTRING", "TLOGICAL", "TBYTE", "TSHORT", "TUSHORT", "TINT", "TUINT", + "TLONG", "TULONG", "TLONGLONG", "TFLOAT", "TDOUBLE", "TCOMPLEX", + "TDBLCOMPLEX" +}; +// Values itself +int type_vals[] = { + TSTRING, + TLOGICAL, + TBYTE, + TSHORT, + TUSHORT, + TINT, + TUINT, + TLONG, + TULONG, + TLONGLONG, + TFLOAT, + TDOUBLE, + TCOMPLEX, + TDBLCOMPLEX +}; + +int VALS_N; // Number of items + +gchar *colnames[] = {N_("Type"), N_("Name"), N_("Value"), N_("Comment")}; +#define VISIBLE_COLS 4 // The amount of visible columns + +int find_list_type(char *type){ + int i; + for(i = 0; i < VALS_N; i++) + if(strcmp(key_types[i], type) == 0) break; + if(i == VALS_N) // Error: there's no such value in list + i = 0; + return i; +} + +gboolean ll_conv(gchar **value, int type){ + long long ll_val, oll_val; + gboolean ret = TRUE; + ll_val = oll_val = strtoll(*value, NULL, 0); + //g_free(*value); + switch(type){ + case TBYTE: + if(ll_val > CHAR_MAX || ll_val < CHAR_MIN) ll_val = 0L; + break; + case TSHORT: + if(ll_val > SHRT_MAX || ll_val < SHRT_MIN) ll_val = 0L; + break; + case TINT: + if(ll_val > INT_MAX || ll_val < INT_MIN) ll_val = 0L; + break; + case TLONG: + if(ll_val > LONG_MAX || ll_val < LONG_MIN) ll_val = 0L; + break; + case TLONGLONG: + if(ll_val > LLONG_MAX || ll_val < LLONG_MIN) ll_val = 0L; + break; + case TUSHORT: + if(ll_val > USHRT_MAX || ll_val < 0L) ll_val = 0L; + break; + case TUINT: + if(ll_val > UINT_MAX || ll_val < 0L) ll_val = 0L; + break; + case TULONG: + if((unsigned long)ll_val > ULONG_MAX || ll_val < 0L) ll_val = 0L; + break; + default: + ll_val = 0L; + break; + } + DBG("long long: %lld", ll_val); + if(ll_val != oll_val){ + g_err(_("Error in value range!")); + ret = FALSE; + } + g_free(*value); + *value = g_strdup_printf("%lld", ll_val); + return ret; +} + +gboolean mystrtod(gchar **val){ + double d_val; + gboolean ret = TRUE; + char *eptr = NULL, *ptr = *val; + errno = 0; + d_val = strtod(ptr, &eptr); + DBG("double: %.16e (was: %s)", d_val, *val); + if((eptr && *eptr) || errno == ERANGE){ + g_free(*val); + DBG("try convert %g into val", d_val); + *val = g_strdup_printf("%.16e", d_val); + g_err(_("Invalid double number!")); + ret = FALSE; + } + DBG("val: %s", *val); + return ret; +} + +gboolean check_complex_number(gchar **val){ + double re = 0., im = 0.; + gboolean ret = TRUE; + if(!cmplx_conv(*val, &re, &im)){ + gchar *msg = g_strdup_printf("%s\n%s", + _("Format error: complex number must be in format"), + "RE + iIM, RE + jIM, RE iIM, RE jIM, (RE, IM) or (RE IM)"); + g_err(msg); + g_free(msg); + ret = FALSE; + } + DBG("complex: (%g, %g)", re, im); + g_free(*val); + *val = g_strdup_printf("(%g, %g)", re, im); + return ret; +} + +gboolean check_val(gchar **newval, int type){ + gboolean b_val, ret = TRUE; + switch(type){ + case TBYTE: + case TSHORT: + case TINT: + case TLONG: + case TLONGLONG: + case TUSHORT: + case TUINT: + case TULONG: + // convert *char -> long long vith checking value + ret = ll_conv(newval, type); + break; + case TFLOAT: + case TDOUBLE: + // convert *char -> double + ret = mystrtod(newval); + break; + case TCOMPLEX: + case TDBLCOMPLEX: + // check whether a number is complex + ret = check_complex_number(newval); + break; + case TLOGICAL: + // convert value into boolean + if(g_ascii_strncasecmp("true", *newval, 4)==0) b_val = TRUE; + else + b_val = atoi(*newval); + g_free(*newval); + *newval = g_strdup_printf("%s", b_val ? "TRUE" : "FALSE"); + DBG("boolean: %s", *newval); + break; + default: + DBG("string: %s", *newval); // all other values a strings + } + return ret; +} + +gboolean try_change_val(IMAGE *ima, gchar **newval){ + int type; + GtkTreeIter cur_items; + gboolean ret; + GtkTreeModel *model; + FNAME(); + if(!gtk_tree_selection_get_selected(sel,&model, &cur_items)) return FALSE; + gtk_tree_model_get(GTK_TREE_MODEL(ima->store), &cur_items, L_TYPE, &type, -1); + ret = check_val(newval, type); + gtk_list_store_set(ima->store, &cur_items, L_VAL, *newval, -1); + DBG("ret"); + return ret; +} +guint cur_column; +void edit_cell( GtkCellRendererText *cell, + gchar *path, + gchar *new_text, + IMAGE *ima){ + GtkTreeModel *model; + GtkTreeIter cur_items; + cur_column = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(cell), "column_num")); + gboolean nxtcol = TRUE, editable = TRUE; + if(!path) return; + DBG("path: %s, new: %s, colnum: %d", path, new_text, cur_column); + gchar *textptr; + if(!gtk_tree_selection_get_selected(sel,&model, &cur_items)) return; + switch(cur_column){ + case L_TYPENM: // key type was changed + gtk_list_store_set(ima->store, &cur_items, L_TYPE, + type_vals[find_list_type(new_text)], -1); + gtk_list_store_set(ima->store, &cur_items, cur_column, new_text, -1); + break; + case L_VAL: // key value was changed + textptr = g_strdup_printf("%s", new_text); + nxtcol = try_change_val(ima, &textptr); // try to change parameter according to its type + g_free(textptr); + break; + default: + gtk_list_store_set(ima->store, &cur_items, cur_column, new_text, -1); + break; + } + if(nxtcol){ + if(cur_column < L_COMM) // it's not a last column - select next + cur_column++; + else{ + cur_column = 0; + editable = FALSE; + } + } + DBG("set cursor col:%d, e=%d", cur_column, editable); + gtk_tree_view_set_cursor(Tree, + gtk_tree_model_get_path(GTK_TREE_MODEL(ima->store), &cur_items), + gtk_tree_view_get_column(Tree, cur_column), editable); + DBG("ret"); +} +/* +void cancel_edit(GtkCellRenderer *renderer){ + cur_column = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(renderer), "column_num")); + gtk_tree_view_set_cursor(Tree, + gtk_tree_model_get_path(GTK_TREE_MODEL(store), &cur_items), + gtk_tree_view_get_column(Tree, cur_column), FALSE); +} +*/ +void add_key( IMAGE *ima, + gchar *keytype, gchar *keyname, gchar *keyval, + gchar *keycomment, int keyfitstype){ + GtkTreeIter iter; + //~ FNAME(); + gtk_list_store_append(ima->store, &iter); + gtk_list_store_set(ima->store, &iter, + L_TYPENM, keytype, + L_KEY, keyname, + L_VAL, keyval, + L_COMM, keycomment, + L_TYPE, keyfitstype, + -1);// the last element must be -1 +} + +void init_keylist(IMAGE *ima){ + /* Create a list model. + * We will have 4 columns: name of key type, key name, key value, comment, type + */ + if(ima->store) g_object_unref(G_OBJECT(ima->store)); // clear list, if it already exists + ima->store = gtk_list_store_new(L_MAX, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT); +} + +GtkWidget *create_treeview(IMAGE *ima){ + int i, i_max = sizeof(key_types)/sizeof(char*);; + GtkTreeIter iter; + GtkCellRenderer *renderer; + GtkTreeViewColumn *column; + if(combo_) + gtk_list_store_clear(combo_); + combo_ = gtk_list_store_new(1, G_TYPE_STRING); + for(i = 0; i < i_max; i++){ + gtk_list_store_append(combo_, &iter); + gtk_list_store_set(combo_, &iter, 0, key_types[i], -1); + } + // create list (treeview) according model store + Tree = (GtkTreeView*)gtk_tree_view_new_with_model(GTK_TREE_MODEL(ima->store)); + // Create columns in list + for(i = 0; i < VISIBLE_COLS; i++){ + if(i == 0){ + renderer = gtk_cell_renderer_combo_new(); + g_object_set(renderer, "text-column", 0, + "editable", TRUE, "has-entry", FALSE, "model", + GTK_TREE_MODEL(combo_), NULL); + } + else{ + renderer = gtk_cell_renderer_text_new (); + g_object_set(renderer, "editable", TRUE, NULL); + } + g_object_set_data(G_OBJECT(renderer), "column_num", GINT_TO_POINTER(i)); + column = gtk_tree_view_column_new_with_attributes(_(colnames[i]), + renderer, "text", i, NULL); + gtk_tree_view_append_column(Tree, column); // insert column + gtk_tree_view_column_set_sort_column_id(column, i);// it may be sorted + g_signal_connect(renderer, "edited", G_CALLBACK(edit_cell), ima); + // g_signal_connect(renderer, "editing-canceled", G_CALLBACK(cancel_edit), NULL); + } +/* + renderer = gtk_cell_renderer_text_new (); + column = gtk_tree_view_column_new_with_attributes ("hidden",renderer, NULL); + g_object_set(column, "visible", FALSE, NULL); + gtk_tree_view_append_column(Tree, column); +*/ + return GTK_WIDGET(Tree); +} + +static void list_changed(GtkTreeSelection *sel){ + GtkTreeModel *model; + GtkTreeIter cur_items; + if(gtk_tree_selection_get_selected(sel,&model, &cur_items)) + gtk_widget_set_sensitive(del_button, TRUE); + else + gtk_widget_set_sensitive(del_button, FALSE); + /* + GtkTreeModel *model; + FNAME(); + gboolean showdel = FALSE; + if(!GTK_IS_TREE_SELECTION(sel)){ + showdel = TRUE; + if(GTK_IS_TREE_SELECTION(obj)) DBG("!!!"); + cur_column = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(sel), "column_num")); + DBG("curcol: %d", cur_column); + } + else if(gtk_tree_selection_get_selected(sel,&model, &cur_items)){ + gint column = L_VAL; + gchar *value; + gtk_tree_model_get(model, &cur_items, column, &value, -1); + DBG("selected: %s", value); + gtk_tree_view_set_cursor(Tree, + gtk_tree_model_get_path(model, &cur_items), + gtk_tree_view_get_column(Tree, cur_column), FALSE); + g_free(value); + showdel = TRUE; + } + gtk_widget_set_sensitive(del_button, showdel); + DBG("ret");*/ +} + +void newline(IMAGE *ima){ + FNAME(); + GtkTreeIter cur_items; + gtk_list_store_append(ima->store, &cur_items); + gtk_list_store_set(ima->store, &cur_items, + L_TYPENM, key_types[0], + L_KEY, "NEW_KEY", + L_VAL, "", + L_COMM, "", + L_TYPE, type_vals[0], + -1); + // Set cursor onto a first field + gtk_tree_view_set_cursor(Tree, + gtk_tree_model_get_path(GTK_TREE_MODEL(ima->store), &cur_items), + gtk_tree_view_get_column(Tree, 0), TRUE); + DBG("ret"); +} + +gboolean del_line(IMAGE *ima){ + GtkTreeIter iter; + GtkTreeModel *model = GTK_TREE_MODEL(ima->store); + gboolean ret = FALSE; + FNAME(); + GtkTreeSelection *selection = gtk_tree_view_get_selection(Tree); + GtkTreePath *path; + if(gtk_tree_selection_get_selected(selection, NULL, &iter)){ + path = gtk_tree_model_get_path(model, &iter); + ret = gtk_list_store_remove(ima->store, &iter); + // check wherher &cur_items was last + if(gtk_list_store_iter_is_valid(ima->store, &iter)){ + gtk_tree_path_free(path); + path = gtk_tree_model_get_path(model, &iter); + } + else gtk_tree_path_prev(path); // goto previous + if(path && gtk_tree_model_get_iter_first(model, &iter)) + gtk_tree_view_set_cursor(Tree, path, NULL, FALSE); + else DBG("No entries"); + gtk_tree_path_free(path); + } + DBG("ret"); + return ret; +} +/* +void show_menu(){ + GtkWidget *menu, *menuitem; + gchar *text; + int i; + menu = gtk_menu_new(); + for(i = 0; i < 10; i++){ + text = g_strdup_printf("%d", i); + menuitem = gtk_menu_item_new_with_label(text); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem); + gtk_widget_show(menuitem); + g_free(text); + } + gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, 3, 0); +} + +gboolean treeview_button_press_cb(GtkWidget *wi, GdkEventButton *ev, gpointer user_data) +{ + GtkTreeSelection *sel; + GtkTreeModel *model; + GtkTreePath *path, *oldpath; + GtkTreeViewColumn *column; + GtkTreeIter iter; + gchar *colname; + + // if there's no path where the click occurred... + if(!gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(wi), ev->x, ev->y, &path, &column, NULL, NULL)) + return FALSE; // ...return FALSE to allow the event to continue + colname = strdup(gtk_tree_view_column_get_title(column)); + // get the tree view's selection + sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(wi)); + + switch(ev->button) + { + // LMB + case 1: + // check to see if this is already the selected path + if(gtk_tree_selection_get_selected(sel, &model, &iter)) + { + oldpath = gtk_tree_model_get_path(model, &iter); + if(gtk_tree_path_compare(oldpath, path) == 0) + { + gtk_tree_path_free(oldpath); + gtk_tree_path_free(path); + return FALSE; + } + gtk_tree_path_free(oldpath); + } + + // select the clicked path + gtk_tree_selection_select_path(sel, path); + return TRUE; + // RMB + case 3: + if(strncmp(colname, "No.", 3)){ + gtk_tree_path_free(path); + return TRUE; + } + show_menu(); +// gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, 3, ev->time); + break; + } + + + // free our path + gtk_tree_path_free(path); + + return FALSE; // allow it to continue +} +*/ + +void close_dlg(){ + gtk_widget_destroy(window); +} + +static gboolean press_key(IMAGE *ima, GdkEventKey * event){ + gboolean ret = FALSE; + if(event->state & (GDK_CONTROL_MASK)){ + switch(event->keyval){ + // ctrl+n inserts a line + case 'n': case 'N': + newline(ima); + ret = TRUE; + break; + // ctrl+d deletes current line + case 'd': case 'D': + ret = del_line(ima); + break; + // ctrl+w closes the window + case 'w': case 'W': + close_dlg(); + } + } + return ret; +} + +void edit_headers(Window *parent){ + VALS_N = sizeof(key_types) / sizeof(gchar *); + GtkWidget *treeview, *vbox, *hbox, *button; + if(!parent->image || !parent->image->store){ + g_err(_("Open fits file first")); + return; + } + window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_title(GTK_WINDOW(window), "List"); + gtk_signal_connect(GTK_OBJECT(window),"delete_event",GTK_SIGNAL_FUNC(gtk_false),NULL); + gtk_signal_connect(GTK_OBJECT(window),"destroy",GTK_SIGNAL_FUNC(close_dlg), NULL); + treeview = create_treeview(parent->image); + sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview)); + g_signal_connect(G_OBJECT(sel), "changed", G_CALLBACK(list_changed), NULL); + //g_signal_connect(G_OBJECT(treeview), "cursor-changed", G_CALLBACK(list_changed), NULL); + + vbox = gtk_vbox_new(FALSE, 5); + gtk_container_add (GTK_CONTAINER (window), vbox); + + GtkWidget *sw = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(sw), GTK_SHADOW_ETCHED_IN); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw), GTK_POLICY_AUTOMATIC, + GTK_POLICY_AUTOMATIC); + gtk_box_pack_start(GTK_BOX(vbox), sw, TRUE, TRUE, 0); + gtk_container_add(GTK_CONTAINER(sw), treeview); + gtk_window_set_default_size(GTK_WINDOW (window), 640, 480); + + //~ gtk_box_pack_start(GTK_BOX(vbox), treeview, TRUE, TRUE, 0); + + hbox = gtk_hbox_new(FALSE, 5); + gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0); + button = gtk_button_new_with_label(_("New entry (ctrl+n)")); + g_signal_connect_swapped(G_OBJECT(button), "clicked", + G_CALLBACK(newline), parent->image); + gtk_box_pack_end(GTK_BOX(hbox), button, FALSE, FALSE, 0); + + del_button = gtk_button_new_with_label(_("Delete entry (ctrl+d)")); + g_signal_connect_swapped(G_OBJECT(del_button), "clicked", + G_CALLBACK(del_line), parent->image); + gtk_box_pack_end(GTK_BOX(hbox), del_button, FALSE, FALSE, 0); + gtk_widget_set_sensitive(del_button, FALSE); + + button = gtk_button_new_with_label(_("Close (ctrl+w)")); + g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(close_dlg), NULL); + gtk_box_pack_end(GTK_BOX(hbox), button, FALSE, FALSE, 0); + g_signal_connect_swapped(G_OBJECT(window), "key-press-event", + G_CALLBACK(press_key), parent->image); + run_modal_window(GTK_WINDOW(window), parent); +} diff --git a/src/fitsview b/src/fitsview new file mode 100755 index 0000000000000000000000000000000000000000..533c09b7d09624b220997b451307ebe8f2d9c590 GIT binary patch literal 264609 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 EFAKbnk^lez literal 0 HcmV?d00001 diff --git a/src/fitsview.c b/src/fitsview.c new file mode 100644 index 0000000..94617a6 --- /dev/null +++ b/src/fitsview.c @@ -0,0 +1,123 @@ +// fitsview.c - main project file +// +// Copyright 2011 Edward V. Emelianoff +// +// This program 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 2 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 this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +// MA 02110-1301, USA. +#include "fitsview.h" +#include "fits.h" +#include "gtk.h" +#include "opengl.h" +#include "open_dialog.h" +#include "filelist.h" + +#define RED "\033[1;31;40m" +#define GREEN "\033[1;32;40m" +#define OLDCOLOR "\033[0;0;0m" + +int (*red)(const char *fmt, ...); +int (*green)(const char *fmt, ...); + +int r_pr_(const char *fmt, ...){ + va_list ar; int i; + printf(RED); + va_start(ar, fmt); + i = vprintf(fmt, ar); + va_end(ar); + printf(OLDCOLOR "\n"); + return i; +} + +int g_pr_(const char *fmt, ...){ + va_list ar; int i; + printf(GREEN); + va_start(ar, fmt); + i = vprintf(fmt, ar); + va_end(ar); + printf(OLDCOLOR "\n"); + return i; +} + + +Global_context *Global; + +// init structures with zeros +void *init_struct(int size){ + int8_t *ptr = calloc(1, size); + return (void*) ptr; +} +// copy structure +void *copy_struct(void *src, int size){ + int8_t *ptr = calloc(1, size); + memcpy(ptr, src, size); + return (void*)ptr; +} + +// current time (for random number generator initialisation & for debug purposes) +double dtime(){ + double t; + struct timeval tv; + gettimeofday(&tv, NULL); + t = tv.tv_sec + ((double)tv.tv_usec)/1e6; + return t; +} + +// if we'll save preferences global context should be read fom file +void init_contexts(){ + // limit spot size + Global->maxSpotH = 100; + Global->minSpotH = 5; + Global->maxSpotW = 100; + Global->minSpotW = 5; +} + +// here will be a function to process command line arguments + + +int main(int argc, char *argv[]){ + gchar *filename = NULL, *str; + if(isatty(STDOUT_FILENO)){ // make color output in tty + red = r_pr_; green = g_pr_; + }else{ // no colors in case of pipe + red = printf; green = printf; + } +#ifdef CUDA_FOUND + DBG("CUDA found"); +#else + DBG("CUDA not found, using CPU instead"); +#endif + bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR); + bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8"); + textdomain(GETTEXT_PACKAGE); + INIT(Global, Global_context); + init_contexts(); + init_main_window(&argc, &argv); + getGLinfo(mainWindow->drawingArea); + setlocale(LC_NUMERIC, "C"); // for right strto[df] working + // Show all for begin + mainWindow->context->visualMode = SHOW_ALL; + mainWindow->image->data = NULL; + if(argc > 1){ + str = basename(argv[1]); + saved_path = get_current_dir_name(); + filename = g_strdup_printf("%s/%s", saved_path, str); + fill_filelist(filename, mainWindow); + change_image(filename, mainWindow); + g_free(filename); + }else + gen_texture(mainWindow->image, mainWindow, FALSE); + gtk_main(); + return 0; +} diff --git a/src/fitsview.glade b/src/fitsview.glade new file mode 100644 index 0000000..7de5fb3 --- /dev/null +++ b/src/fitsview.glade @@ -0,0 +1,1358 @@ + + + + + + + + True + vertical + + + True + + + True + _File + True + + + True + + + gtk-open + True + True + True + + + + + + gtk-save-as + True + True + True + + + + + Open in _new window + True + True + False + + + + True + gtk-open + + + + + + + Open in _3D window + True + True + False + + + + True + gtk-open + + + + + + + True + + + + + _Quit + True + True + False + + + True + + + + + + + + + + + _Edit + True + + + + + True + _View + True + + + True + + + True + Show _histogram + True + + + + + + True + Show _headers + True + + + + + + True + + + + + True + 3D view of full image + True + + + + + + True + 3D view of subframe + True + + + + + + True + Select _spots + True + + + + + + True + Draw _tracks + True + + + + + + True + + + + + True + _Zoom frame + True + + + + + + True + _Restore image + True + + + + + + True + Zoom _in + True + + + + + + True + Zoom _out + True + + + + + + + + + + True + _Math + True + + + True + + + True + Find and enumerate spots + _Spots + True + + + True + + + True + Choose minimal & maximal spot size + Size _tresholds + True + + + + + + True + Identify _spots + True + + + + + + True + So_rt hartmann spots + True + + + + + + True + Sa_ve spots + True + + + + + + + + + + True + Identify _circles + True + + + + + + True + _Hough transform + True + + + + + + True + _Filter + True + + + + + + + + + + True + _Help + True + + + True + + + gtk-about + True + True + True + + + + + + + + + + False + 0 + + + + + + + + True + 3 + 3 + + + 400 + 400 + True + + + 1 + 3 + 2 + + + + + True + 10 + 5 + 10 + + + 1 + 3 + 2 + 3 + GTK_EXPAND | GTK_SHRINK | GTK_FILL + GTK_FILL + + + + + True + vertical + 10 + 5 + 10 + + + 2 + GTK_FILL + GTK_EXPAND | GTK_SHRINK | GTK_FILL + + + + + + + + 2 + + + + + True + 1 + + + True + True + False + False + + True + none + + + 0 + + + + + True + True + False + False + + True + none + + + False + 1 + + + + + True + True + False + False + + True + none + + + False + 2 + + + + + True + True + False + False + + True + none + + + False + 3 + + + + + False + 3 + + + + + + + + + True + vertical + + + True + + + True + _File + True + + + True + + + True + + + + + gtk-close + True + True + True + + + + + + + + + + + _Edit + True + + + + + True + _View + True + + + True + + + True + Y axis scale + True + + + True + + + True + Linear + True + True + True + + + + + + True + Log + True + True + graphLinMenuItem + + + + + + + + + + + + + + True + _Math + True + + + True + + + True + Approximate by _gaussian + True + + + + + + + + + + False + 0 + + + + + + + + True + 3 + 3 + + + 400 + 400 + True + + + 1 + 3 + 2 + + + + + True + 10 + 5 + 10 + + + 1 + 3 + 2 + 3 + GTK_EXPAND | GTK_SHRINK | GTK_FILL + GTK_FILL + + + + + True + vertical + 10 + 5 + 10 + + + 2 + GTK_FILL + GTK_EXPAND | GTK_SHRINK | GTK_FILL + + + + + + + + 2 + + + + + True + + + True + True + False + False + + True + none + + + 0 + + + + + True + True + False + 8 + False + + 8 + True + none + + + False + 1 + + + + + True + True + False + 40 + False + + 28 + True + none + + + False + False + end + 2 + + + + + False + 3 + + + + + + + + + True + vertical + + + True + + + True + _File + True + + + True + + + gtk-open + True + True + True + + + + + + True + + + + + gtk-close + True + True + True + + + + + + + + + + _Edit + True + + + + + True + _View + True + + + True + + + True + _Move + True + + + True + + + True + Rotate X CW + True + + + + + + True + Rotate X CCW + True + + + + + + True + Rotate Z CCW + True + + + + + + True + Rotate Z CW + True + + + + + + True + Move right + True + + + + + + True + Move left + True + + + + + + True + Move down + True + + + + + + True + Move up + True + + + + + + True + Move backward + True + + + + + + True + Move forward + True + + + + + + + + + + True + + + + + True + Mouse and arrow keys navigation + _Game mode + True + + + + + + True + _Restore image + True + + + + + + + + + + True + _Math + True + + + + + False + 0 + + + + + + + + 400 + 400 + True + + + 2 + + + + + True + 1 + + + True + True + False + False + + True + none + + + 0 + + + + + True + True + False + False + + True + none + + + False + 1 + + + + + True + True + False + False + + True + none + + + False + 2 + + + + + True + True + False + False + + True + none + + + False + 3 + + + + + False + 3 + + + + + + + Spots' size treshold + False + True + center-on-parent + True + + + True + 7 + 7 + 7 + 7 + + + True + vertical + 7 + + + True + 7 + True + + + True + 0 + out + + + True + 12 + + + True + vertical + 4 + True + + + True + 5 + + + True + Min + + + False + False + 0 + + + + + True + True + + + + 1 + + + + + False + False + 0 + + + + + True + 5 + + + True + Max + + + False + False + 0 + + + + + True + True + + + + 1 + + + + + False + False + 1 + + + + + + + + + True + <b>Width</b> + True + + + label_item + + + + + 0 + + + + + True + 0 + in + + + True + 12 + + + True + vertical + 5 + True + + + True + 5 + + + True + Min + + + False + False + 0 + + + + + True + True + + + + 1 + + + + + False + False + 0 + + + + + True + 5 + + + True + Max + + + False + False + 0 + + + + + True + True + + + + 1 + + + + + False + False + 1 + + + + + + + + + True + <b>Height</b> + True + + + label_item + + + + + 1 + + + + + 0 + + + + + True + 10 + True + + + gtk-cancel + True + True + True + True + + + False + 0 + + + + + gtk-ok + True + True + True + True + + + False + 1 + + + + + False + False + 1 + + + + + + + + + 5 + normal + False + + + True + vertical + 2 + + + True + 7 + 7 + 7 + + + True + 7 + + + True + 0 + none + + + True + 12 + + + True + True + + + + + + + + True + <b>Focus value, mm</b> + True + + + label_item + + + + + 0 + + + + + True + 0 + none + + + True + 12 + + + True + vertical + 5 + True + + + Prefocal + True + True + False + True + True + + + 0 + + + + + Postfocal + True + True + False + True + FocalPreRadioBtn + + + 1 + + + + + + + + + True + <b>Image type</b> + True + + + label_item + + + + + 1 + + + + + + + 1 + + + + + True + center + + + + + + gtk-ok + True + True + True + True + + + False + False + 1 + + + + + False + end + 0 + + + + + + diff --git a/src/fitsview.ui b/src/fitsview.ui new file mode 100644 index 0000000..cf9493e --- /dev/null +++ b/src/fitsview.ui @@ -0,0 +1,1281 @@ + + + + + + + + mainFileMenuItem + _File + + + + + gtk-open + mainOpenMenuItem + + + + + + gtk-save-as + mainSaveAsMenuItem + + + + + gtk-open + mainOpenInNewMenuItem + Open in _new window + + + + + + gtk-open + mainOpenIn3DMenuItem + Open in _3D window + + + + + + mainQuitMenuItem + _Quit + + + + + mainEditMenuItem + _Edit + + + + + mainViewMenuItem + _View + + + + + mainHistMenuItem + Show _histogram + + + + + + mainHeadersMenuItem + Show _headers + + + + + + main3DviewFullMenuItem + 3D view of full image + + + + + + main3DviewFrameMenuItem + 3D view of subframe + + + + + + mainSpotsidentMenuItem + Select _spots + + + + + + mainTrackMenuItem + Draw _tracks + + + + + + mainZoomframeMenuItem + _Zoom frame + + + + + + mainZoomrestoreMenuItem + _Restore image + + + + + + mainZoomnearMenuItem + Zoom _in + + + + + + mainZoomfarMenuItem + Zoom _out + + + + + + mainMathMenuItem + _Math + + + + + mainSpotsMenuItem + Find and enumerate spots + _Spots + + + + + mainSpotsParamMenuItem + Choose minimal & maximal spot size + Size _tresholds + + + + + + mainFindSpotsMenuItem + Identify _spots + + + + + + mainHartmannMenuItem + So_rt hartmann spots + + + + + + mainSaveSpotsMenuItem + Sa_ve spots + + + + + + mainCirclesMenuItem + Identify _circles + + + + + + mainHoughMenuItem + _Hough transform + + + + + + mainFilterMenuItem + _Filter + + + + + + mainHelpMenuItem + _Help + + + + + gtk-about + mainAboutMenuItem + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + graphFileMenuItem + _File + + + + + gtk-close + graphCloseMenuItem + + + + + + + graphEditMenuItem + _Edit + + + + + graphViewMenuItem + _View + + + + + graphScaleMenuItem + Y axis scale + + + + + True + graphLinMenuItem + Linear + + + + + + graphLinMenuItem + graphLogMenuItem + Log + + + + + + graphMathMenuItem + _Math + + + + + graphGaussMenuItem + Approximate by _gaussian + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + oglFileMenuItem + _File + + + + + gtk-open + oglOpenMenuItem + + + + + + gtk-close + oglCloseMenuItem + + + + + + oglEditMenuItem + _Edit + + + + + oglViewMenuItem + _View + + + + + oglMoveMenuItem + _Move + + + + + oglXAplusMenuItem + Rotate X CW + + + + + + oglXAminusMenuItem + Rotate X CCW + + + + + + oglZAplusMenuItem + Rotate Z CCW + + + + + + oglZAminusMenuItem + Rotate Z CW + + + + + + oglXplusMenuItem + Move right + + + + + + oglXminusMenuItem + Move left + + + + + + oglYplusMenuItem + Move down + + + + + + oglYminusMenuItem + Move up + + + + + + oglZplusMenuItem + Move backward + + + + + + oglZminusMenuItem + Move forward + + + + + + oglGameModeMenuItem + Mouse and arrow keys navigation + _Game mode + + + + + + oglZoomrestoreMenuItem + _Restore image + + + + + + oglMathMenuItem + _Math + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + vertical + + + True + + + False + 0 + + + + + + + + True + 3 + 3 + + + 400 + 400 + True + + + 1 + 3 + 2 + + + + + True + 10 + 5 + 10 + + + 1 + 3 + 2 + 3 + GTK_EXPAND | GTK_SHRINK | GTK_FILL + GTK_FILL + + + + + True + vertical + 10 + 5 + 10 + + + 2 + GTK_FILL + GTK_EXPAND | GTK_SHRINK | GTK_FILL + + + + + + + + 2 + + + + + True + 1 + + + True + True + False + False + + True + none + + + 0 + + + + + True + True + False + False + + True + none + + + False + 1 + + + + + True + True + False + False + + True + none + + + False + 2 + + + + + True + True + False + False + + True + none + + + False + 3 + + + + + False + 3 + + + + + + + + + True + vertical + + + True + + + False + 0 + + + + + + + + True + 3 + 3 + + + 400 + 400 + True + + + 1 + 3 + 2 + + + + + True + 10 + 5 + 10 + + + 1 + 3 + 2 + 3 + GTK_EXPAND | GTK_SHRINK | GTK_FILL + GTK_FILL + + + + + True + vertical + 10 + 5 + 10 + + + 2 + GTK_FILL + GTK_EXPAND | GTK_SHRINK | GTK_FILL + + + + + + + + 2 + + + + + True + + + True + True + False + False + + True + none + + + 0 + + + + + True + True + False + 8 + False + + 8 + True + none + + + False + 1 + + + + + True + True + False + 40 + False + + 28 + True + none + + + False + False + end + 2 + + + + + False + 3 + + + + + + + + + True + vertical + + + True + + + False + 0 + + + + + + + + 400 + 400 + True + + + 2 + + + + + True + 1 + + + True + True + False + False + + True + none + + + 0 + + + + + True + True + False + False + + True + none + + + False + 1 + + + + + True + True + False + False + + True + none + + + False + 2 + + + + + True + True + False + False + + True + none + + + False + 3 + + + + + False + 3 + + + + + + + Spots' size treshold + False + True + center-on-parent + True + + + True + 7 + 7 + 7 + 7 + + + True + vertical + 7 + + + True + 7 + True + + + True + 0 + out + + + True + 12 + + + True + vertical + 4 + True + + + True + 5 + + + True + Min + + + False + False + 0 + + + + + True + True + + + + 1 + + + + + False + False + 0 + + + + + True + 5 + + + True + Max + + + False + False + 0 + + + + + True + True + + + + 1 + + + + + False + False + 1 + + + + + + + + + True + <b>Width</b> + True + + + + + 0 + + + + + True + 0 + in + + + True + 12 + + + True + vertical + 5 + True + + + True + 5 + + + True + Min + + + False + False + 0 + + + + + True + True + + + + 1 + + + + + False + False + 0 + + + + + True + 5 + + + True + Max + + + False + False + 0 + + + + + True + True + + + + 1 + + + + + False + False + 1 + + + + + + + + + True + <b>Height</b> + True + + + + + 1 + + + + + 0 + + + + + True + 10 + True + + + gtk-cancel + True + True + True + True + + + False + 0 + + + + + gtk-ok + True + True + True + True + + + False + 1 + + + + + False + False + 1 + + + + + + + + + 5 + normal + False + + + True + vertical + 2 + + + True + 7 + 7 + 7 + + + True + 7 + + + True + 0 + none + + + True + 12 + + + True + True + + + + + + + + True + <b>Focus value, mm</b> + True + + + + + 0 + + + + + True + 0 + none + + + True + 12 + + + True + vertical + 5 + True + + + Prefocal + True + True + False + True + True + + + 0 + + + + + Postfocal + True + True + False + True + FocalPreRadioBtn + + + 1 + + + + + + + + + True + <b>Image type</b> + True + + + + + 1 + + + + + + + 1 + + + + + True + center + + + + + + gtk-ok + True + True + True + True + + + False + False + 1 + + + + + False + end + 0 + + + + + + diff --git a/src/gauss.c b/src/gauss.c new file mode 100644 index 0000000..64cdcf7 --- /dev/null +++ b/src/gauss.c @@ -0,0 +1,378 @@ +// gauss.c - funtions for gaussian approximation +// +// Copyright 2011 Edward V. Emelianoff +// +// This program 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 2 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 this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +// MA 02110-1301, USA. +#include "gtk.h" +#include "gauss.h" +#ifdef GSL_FOUND // cmake found GSL library +#include +#include +#include +#include +#include + + +#undef DBG +#undef EBUG +#define DBG(...) + + +/* + * Returns value of gaussian value in point x + * x0, C, A, sigma - are parameters (middle, vertical shift, amplitude and std) + */ +double gaussian_pt(double C, double A, double sigma, double x0, double x){ + double s2 = sigma*sigma; + double ss = A/sqrt(2*M_PI*s2); + double E = x-x0; + return C + ss*exp(-E*E/2/s2); +} + +/* + * Fills the ordinates of structure Points by Gaussian + * (Abscissa does not move, so that this structure can be used + * for multiple rendering of the same area but with different + * parameters) + */ +void gaussian_v(double C, double A, double sigma, double x0, Points *pts){ + int i, n = pts->n; + Point *pt = pts->data; + double s2 = sigma*sigma * 2; + double ss = A/sqrt(2*M_PI*s2); + double E; + for(i = 0; i < n; i++, pt++){ + E = pt->x - x0; + pt->y = C + ss*exp(-E*E/s2); + //DBG("pt %d (%g, %g)", i, pt->x, pt->y); + } +} + +int gauss_f(const gsl_vector *parms, void *data, gsl_vector *f){ + size_t n = ((struct data *)data)->n; + double *x = ((struct data *)data)->x; + double *y = ((struct data *)data)->y; + double *dy = ((struct data *)data)->dy; + double x0 = gsl_vector_get(parms, 3); + double sigma = gsl_vector_get(parms, 2); + double A = gsl_vector_get(parms, 1) * 0.65; // without 0.65 A would be wrong (WTF?) + double C = gsl_vector_get(parms, 0); + size_t i; + double mul = A/fabs(sigma)/sqrt(2.*M_PI); + for (i = 0; i < n; i++){ + /* Model Yi = C + A/sqrt(2*pi*sigma^2) * exp(-(x-b)^2/2/sigma^2) */ + double E = (x[i]-x0)/sigma; + double Yi = C + mul * exp(-E * E/2.); + gsl_vector_set(f, i, (Yi - y[i])/dy[i]); + } + return GSL_SUCCESS; +} + +int gauss_df(const gsl_vector *parms, void *data, gsl_matrix *J){ + size_t n = ((struct data *)data)->n; + double *x = ((struct data *)data)->x; + double *dy = ((struct data *) data)->dy; + double A = gsl_vector_get(parms, 1); + double sigma = gsl_vector_get(parms, 2); + double x0 = gsl_vector_get(parms, 3); + double S2PI = 1. / sqrt(2.*M_PI); + size_t i; + for(i = 0; i < n; i++){ + /* Jacobian matrix J(i,j) = dfi / dxj, */ + /* and the xj are the parameters (C, A,sigma,x0) */ + double s = dy[i]; + double xbs = (x[i]-x0)/sigma; + double y = exp(-xbs*xbs/2.) *S2PI / fabs(sigma) / s; + double Ays = A*y/sigma; + // df/dx0 = f * (x-x0)/sigma^2 + gsl_matrix_set(J, i, 3, Ays*xbs); + // df/dsigma = -f(1-(x-x0)^2/sigma^2)/sigma + gsl_matrix_set(J, i, 2, Ays*(xbs*xbs-1.)); + gsl_matrix_set(J, i, 1, y); // df/dA + gsl_matrix_set(J, i, 0, 1. / s); // df/dC + } + return GSL_SUCCESS; +} + +int gauss_fdf(const gsl_vector *parms, void *data, gsl_vector *f, gsl_matrix *J){ + gauss_f(parms, data, f); + gauss_df(parms, data, J); + return GSL_SUCCESS; +} + +// pseudorandom numbers generator initialisation +void urandom_ini(){ + double tt = dtime() * 1e6; + double mx = (double)LONG_MAX; + srand48((long)(tt - mx * floor(tt/mx))); +} + + +#define FIT(i) gsl_vector_get(s->x, i) +#define ERR(i) sqrt(gsl_matrix_get(covar,i,i)) +#define FN(i) gsl_vector_get(s->f, i) +/* + * Gaussian parameters calculation y=A/sqrt(2*pi*sigma^2) exp(-(x-x_0)^2/2/sigma^2), + * which approximates the points set pts + * Parameters A_, sigma_, x0_ may be NULL (if you don't need any of them) + */ +void gauss_fit(Points *pts, double *C_, double *A_, double *sigma_, double *x0_){ + // VVVV lower parameters may be formed as a structure to change as function argument + double + epsabs = 1e-8,// absolute error + epsrel = 1e-5,// relative error + chi_max = 0.01;// max chi value for iterations criteria + int max_iter = 300; // limit iterations number of gsl_multifit_fdfsolver + size_t N_MIN = 10; // minimum points for approximation + double x_init[4]; + // AAAA upper parameters may be formed as a structure to change as function argument +/* x_init, the best approximations: + * x0 - not far from real (the nearest is the better) + * sigma - not far from real (the nearest is the better) + * A - not large ~10 (it has a weak effect) + */ + const gsl_multifit_fdfsolver_type *T; + gsl_multifit_fdfsolver *s; + int status; + #ifdef EBUG + int appNo = 0; + #endif + int iter; + size_t i, j, n = pts->n, oldn; + const size_t p = 4; + gsl_matrix *covar = gsl_matrix_alloc (p, p); + #ifdef EBUG + double t0; + #endif + double *x, *y, *dy, chi, C, A, sigma, x0; + if(n < 1) return; + x = malloc(n * sizeof(double)); + y = malloc(n * sizeof(double)); + dy = malloc(n * sizeof(double)); + struct data d = {n, x, y, dy}; + gsl_multifit_function_fdf f; + gsl_vector_view xx = gsl_vector_view_array(x_init, p); + const gsl_rng_type *type; + gsl_rng *r; + + gsl_rng_env_setup(); + type = gsl_rng_default; + r = gsl_rng_alloc (type); + f.f = &gauss_f; + f.df = &gauss_df; + f.fdf = &gauss_fdf; + f.n = n; + f.p = p; + f.params = &d; + // fill data structure. Don't forget Okkam's razor!!! + { + Point *pt = pts->data; + double *px = x, *py = y, *pdy = dy, sum = 0.; + for(i = 0; i < n; i++, pt++){ + *pdy++ = 1.; // I have no idea what is it, so init by 1 + *px++ = pt->x; + *py++ = pt->y; + sum += pt->y; + //DBG("point %d: (%g, %g)", i, pt->x, pt->y); + } + // fill x_init: x0, sigma, C, A (it can be a funtion parameter) + x_init[3] = (*(--px) + *x) / 2.; + x_init[2] = fabs((*x - *px) / 4.); + x_init[0] = sum/(double)n; + x_init[1] = sum; + DBG("\nInitial parameters: x0=%.1f, sigma=%.1f, A=%.1f, C=%.1f", + x_init[3], x_init[2], x_init[1], x_init[0]); + } + T = gsl_multifit_fdfsolver_lmder; // or also gsl_multifit_fdfsolver_lmsder + s = gsl_multifit_fdfsolver_alloc(T, n, p); + #ifdef EBUG + t0 = dtime(); + #endif + do{ + double dof, tres, c; + DBG("\n************ Approximation %d ******************\n", appNo++); + iter = 0; + gsl_multifit_fdfsolver_set(s, &f, &xx.vector); + do{ + iter++; + status = gsl_multifit_fdfsolver_iterate(s); + if(status) + break; + status = gsl_multifit_test_delta(s->dx, s->x, epsabs, epsrel); + }while(status == GSL_CONTINUE && iter < max_iter); + DBG("time=%g\n", dtime()-t0); + gsl_multifit_covar(s->J, 0.0, covar); + chi = gsl_blas_dnrm2(s->f); + dof = n - p; + tres = chi; + c = chi / sqrt(dof); // GSL_MAX_DBL(1., chi / sqrt(dof)); + C = FIT(0), A = FIT(1), sigma = FIT(2), x0 = FIT(3); + DBG("Number of iteratons = %d\n", iter); + DBG("chi = %g, chi/dof = %g\n", chi, chi / sqrt(dof)); + DBG("C = %.5f +/- %.5f\n", C, c*ERR(0)); + DBG("A = %.5f +/- %.5f\n", A, c*ERR(1)); + DBG("sigma = %.5f +/- %.5f\n", sigma, c*ERR(2)); + DBG("x0 = %.5f +/- %.5f\n", x0, c*ERR(3)); + j = 0; + oldn = n; + if(c < chi_max) break; + // throw out bad (by chi) data + for(i = 0; i < n; i++){ + if(fabs(FN(i)) < tres){ + if(i != j){ + x[j] = x[i]; + y[j] = y[i]; + dy[j] = dy[i]; + } + j++; continue; + } + } + if(j != n){ + DBG("Chi tresholding %g, %zd points of %zd\n", tres, j, n); + n = j; + d.n = n; + } + }while(chi > chi_max && n != oldn && n > N_MIN); + if(C_) *C_ = C; + if(A_) *A_ = A; + if(sigma_) *sigma_ = sigma; + if(x0_) *x0_ = x0; + //printf ("status = %s\n", gsl_strerror (status)); + gsl_multifit_fdfsolver_free(s); + gsl_matrix_free(covar); + gsl_rng_free(r); + free(x); free(y); free(dy); +} +/* +int circle_f(const gsl_vector *parms, void *data, gsl_vector *f){ + size_t n = ((struct data *)data)->n; + double *x = ((struct data *)data)->x; + double *y = ((struct data *)data)->y; + //double *dy = ((struct data *)data)->dy; + double X = gsl_vector_get(parms, 0); + double Y = gsl_vector_get(parms, 1); + double R = gsl_vector_get(parms, 2); + double sign, Yi, DX; + size_t i; + for (i = 0; i < n; i++){ + sign = (y[i] > Y) ? 1. : -1.; + DX = x[i] - X; + // Model Yi = Yc +- sqrt[R^2-(x-Xc)^2] + Yi = Y + sign * sqrt(R*R - DX*DX); + gsl_vector_set(f, i, (Yi - y[i])); + } + return GSL_SUCCESS; +} + +int circle_df(const gsl_vector *parms, void *data, gsl_matrix *J){ + size_t n = ((struct data *)data)->n; + double *x = ((struct data *)data)->x; + double *y = ((struct data *)data)->y; + //double *dy = ((struct data *)data)->dy; + double X = gsl_vector_get(parms, 0); + double Y = gsl_vector_get(parms, 1); + double R = gsl_vector_get(parms, 2); + double sign, Yi, DX; + size_t i; + for(i = 0; i < n; i++){ + // Jacobian matrix J(i,j) = dfi / dxj, + // and the xj are the parameters (Xc, Yc, R) + DX = x[i] - X; + sign = (y[i] > Y) ? 1. : -1.; + Yi = sign / sqrt(R*R - DX*DX); + gsl_matrix_set(J, i, 2, R*Yi); // df/dR + gsl_matrix_set(J, i, 1, 1.); // df/dYc + gsl_matrix_set(J, i, 0, DX * Yi); // df/dXc + } + return GSL_SUCCESS; +} + +int circle_fdf(const gsl_vector *parms, void *data, gsl_vector *f, gsl_matrix *J){ + circle_f(parms, data, f); + circle_df(parms, data, J); + return GSL_SUCCESS; +} + +void circle_fit(struct data *d, double *Xcenter, + double *Ycenter, double *Radius){ + double + epsabs = 1e-8, + epsrel = 1e-5; + int max_iter = 30; + double x_init[3], c, chi; + const gsl_multifit_fdfsolver_type *T; + gsl_multifit_fdfsolver *s; + int status; + int iter = 0; + size_t n = d->n; + const size_t p = 3; + FNAME(); + gsl_matrix *covar = gsl_matrix_alloc (p, p); + gsl_multifit_function_fdf f; + gsl_vector_view xx = gsl_vector_view_array(x_init, p); + const gsl_rng_type *type; + gsl_rng *r; + gsl_rng_env_setup(); + type = gsl_rng_default; + r = gsl_rng_alloc(type); + f.f = &circle_f; + f.df = &circle_df; + f.fdf = &circle_fdf; + f.n = n; + f.p = p; + f.params = d; + T = gsl_multifit_fdfsolver_lmder; + s = gsl_multifit_fdfsolver_alloc(T, n, p); + gsl_multifit_fdfsolver_set(s, &f, &xx.vector); + x_init[0] = *Xcenter; + x_init[1] = *Ycenter; + x_init[2] = *Radius; + do{ + iter++; + status = gsl_multifit_fdfsolver_iterate(s); + if(status) break; + DBG("iter %d", iter); + status = gsl_multifit_test_delta(s->dx, s->x, epsabs, epsrel); + }while(status == GSL_CONTINUE && iter < max_iter); + DBG("iter end"); + gsl_multifit_covar(s->J, 0.0, covar); + chi = gsl_blas_dnrm2(s->f); + c = chi / sqrt((double)(n - p)); + *Xcenter = FIT(0); + *Ycenter = FIT(1); + *Radius = FIT(2); + DBG("Number of iteratons = %d\n", iter); + DBG("chi = %g, chi/dof = %g\n", chi, c); + DBG("Xc = %.5f +/- %.5f\n", *Xcenter, c*ERR(0)); + DBG("Yc = %.5f +/- %.5f\n", *Ycenter, c*ERR(1)); + DBG("R = %.5f +/- %.5f\n", *Radius, c*ERR(2)); + gsl_multifit_fdfsolver_free(s); + gsl_matrix_free(covar); + gsl_rng_free(r); +} +*/ + +#else // GSL not found + +//double gaussian_pt(double C, double A, double sigma, double x0, double x){ +// GSLERR; return A+C+sigma+x+x0;} +void gaussian_v(double C __attribute__((unused)), double A __attribute__((unused)), + double sigma __attribute__((unused)), double x0 __attribute__((unused)), + Points *pts __attribute__((unused))){GSLERR;} +void gauss_fit(Points *pts __attribute__((unused)), double *C_ __attribute__((unused)), + double *A_ __attribute__((unused)), double *sigma_ __attribute__((unused)), + double *x0_ __attribute__((unused))){ GSLERR;} +#endif // GSL_FOUND diff --git a/src/gtk.c b/src/gtk.c new file mode 100644 index 0000000..c860139 --- /dev/null +++ b/src/gtk.c @@ -0,0 +1,1204 @@ +// gtk.c - main funtions to work with windows +// +// Copyright 2011 Edward V. Emelianoff +// +// This program 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 2 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 this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +// MA 02110-1301, USA. +#include "gtk.h" +#include "tracking.h" +#include "opengl.h" +#include "ui.h" +#include "imtools.h" +#include "contours.h" +#include "fits.h" +#include "open_dialog.h" +#include "fitsheaders.h" +#include "terrain.h" +#include "filelist.h" +#include + +Window *mainWindow; + +double X0, Y0, lX0, lY0; // started coordinates of click, layer coordinates at click +// the longer key is pressed, the larger value of key_acceleration (3D moving): +double key_acceleration = 1.; +// multipliers for zoom in/out +const double ZOOM_NEAR = 1.1, ZOOM_FAR = 1./1.1; + +void run_modal_window(GtkWindow *w, Window *parent){ + gtk_window_set_transient_for(w, GTK_WINDOW(parent->window)); + gtk_window_set_modal(w, TRUE); + gtk_window_set_position(w, GTK_WIN_POS_MOUSE); + gtk_widget_show_all(GTK_WIDGET(w)); +} + +gint run_modal_dialog(GtkDialog *dialog, Window *parent){ + run_modal_window(GTK_WINDOW(dialog), parent); + return gtk_dialog_run(GTK_DIALOG(dialog)); +} + +/* + * Error messages output + * text - message text + */ +void g_err(gchar *text){ + GtkMessageDialog *dialog; + g_printerr(_("Error: %s\n"), text); + dialog = GTK_MESSAGE_DIALOG(gtk_message_dialog_new(NULL, + GTK_DIALOG_MODAL, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_CLOSE, + "%s", text)); + run_modal_dialog(GTK_DIALOG(dialog), mainWindow); + gtk_widget_destroy(GTK_WIDGET(dialog)); +} + +void show_about_dialog(){ + GtkAboutDialog *about = GTK_ABOUT_DIALOG(gtk_about_dialog_new()); + const gchar *author[] = { "Edward V. Emelianov , ", NULL }; + gtk_about_dialog_set_program_name(about, "FITS view & Hartman data reduction"); + gtk_about_dialog_set_version(about, PACKAGE_VERSION); + gtk_about_dialog_set_copyright(about, "Copyright 2012 Edward V. Emelianov"); + gtk_about_dialog_set_website(about, "http://eddyem.narod.ru"); + gtk_about_dialog_set_authors(about, author); + run_modal_dialog(GTK_DIALOG(about), mainWindow); + gtk_widget_destroy(GTK_WIDGET(about)); +} + +/* + * exit + */ +void chk_quit(){ + gtk_main_quit(); + // uncomment next, when autosaving of settings will be released + /*GtkMessageDialog *dialog; + dialog = GTK_MESSAGE_DIALOG(gtk_message_dialog_new(NULL, + GTK_DIALOG_MODAL, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_YES_NO, + _("You sure, you want exit?"))); + gint ans = run_modal_dialog(GTK_DIALOG(dialog), mainWindow); + gtk_widget_destroy(GTK_WIDGET(dialog)); + if(ans == GTK_RESPONSE_YES){ + gtk_main_quit(); + }*/ +} + +void change_window_filename(gchar *filename, Window *window){ + gchar *title, *bsnm; + if(!filename) return; + g_free(window->image->filename); + bsnm = g_path_get_basename(filename); + if(window == mainWindow) + title = g_strdup_printf("%s (main) - %s", PRGNAME, bsnm); + else + title = g_strdup_printf("%s - %s", PRGNAME, bsnm); + gtk_window_set_title(GTK_WINDOW(window->window), title); + window->image->filename = bsnm; + g_free(title); +} + +void change_image(gchar *filename, Window *window){ + if(!filename) return; + if(try_open_file(filename, window->image)){ + change_window_filename(filename, window); + }else + change_window_filename("", window); + window->context->current_image = window->image; + gen_texture(window->image, window, FALSE); + window->context->transformMask = TRANS_NONE; +} + +// "save as" filename choosing dialog +gchar *get_save_filename(Window *window){ + FNAME(); + GtkFileChooser *dialog; + gchar *filename = NULL, *retfilename = NULL; + gint response; + dialog = GTK_FILE_CHOOSER( + gtk_file_chooser_dialog_new("Save File", NULL, + GTK_FILE_CHOOSER_ACTION_SAVE, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT, + NULL)); + if(saved_path) + gtk_file_chooser_set_current_folder(dialog, saved_path); + gtk_file_chooser_set_current_name(dialog, "new_fits_file.fit"); + gtk_file_chooser_set_do_overwrite_confirmation(dialog, TRUE); + response = run_modal_dialog(GTK_DIALOG(dialog), window); + if(response == GTK_RESPONSE_ACCEPT){ + filename = gtk_file_chooser_get_filename(dialog); + } + gtk_widget_destroy(GTK_WIDGET(dialog)); + if(filename){ + if(!get_ext(filename)) // There wasn't .fit suffix + retfilename = g_strdup_printf("!%s.fit", filename); // to rewrite file, if needed + else + retfilename = g_strdup_printf("!%s", filename); + g_free(filename); + } + return retfilename; +} + +// get choosed filename to open and fills filelist of current directory +gchar *get_open_filename(Window *window){ + FNAME(); + GtkFileChooser *dialog; + gchar *filename = NULL; + gint response; + dialog = open_fits_dialog(); + response = run_modal_dialog(GTK_DIALOG(dialog), window); + g_free(saved_path); + saved_path = gtk_file_chooser_get_current_folder(dialog); + if(response == GTK_RESPONSE_ACCEPT){ + filename = gtk_file_chooser_get_filename(dialog); + DBG("open file %s", filename); + gint nfiles = fill_filelist(filename, window); + DBG("found %d files", nfiles); + g_free(filename); filename = NULL; + if(nfiles > 0 && window->files.list_current) + filename = window->files.list_current->data; + else + g_err(_("Can't read fits file")); + } + gtk_widget_destroy(GTK_WIDGET(dialog)); + return filename; +} + +void save_as(Window *window){ + gchar *filename = NULL; + if((filename = get_save_filename(window))){ + change_window_filename(filename, window); + if(!writefits(filename, window->image)) + g_err(_("Can't save file")); + g_free(filename); + } +} +void open_file(Window *window){ + gchar *filename; + if((filename = get_open_filename(window))) + change_image(filename, window); +} +void open_in_new(Window *window){ + gchar *filename; + Window *newwin = init_window(window, OPENGL_WINDOW); + if(!(filename = get_open_filename(newwin))) + destroy_window(newwin); + else + change_image(filename, newwin); +} +void open_in_3D(Window *window){ + Window *terra = init_window(window, GL3D_WINDOW); + terrain_3D(terra, 0.,0.,0.,0., TRUE); +} + +void close_subwindow(Window *w){ + gtk_widget_hide(w->window); +} + +// Refreshing State fields of status bar +void refresh_state(Window *window){ + //FNAME(); + Context *c = window->context; + gchar buf[32]; + gchar draw[] = {'T', 'S', 'Q'}; + gchar track[] = {'A', 'V', 'H', 'D'}; + gchar graph[] = {'G', 'H'}; + if(window->id != GRAPH_WINDOW){ + g_sprintf(buf, "dX=%.1f, dY=%.1f", window->move.x, window->move.y); + set_status_text(StatusAdd, buf, window); + g_sprintf(buf, "%c %c %s (%.1f%%) Z=%.1f", + draw[c->drawingMode], track[c->trackingMode], + (c->transformMask ? "tr" : ""), + 100./window->Daspect*window->Zoom, window->Zoom); + set_status_text(StatusState, buf, window); + }else{ + g_sprintf(buf, "%c %s", + graph[c->graphMode], (isYlog(window) ? "log" : "lin")); + set_status_text(StatusState, buf, window); + } +} + +void set_status_text(guint barName, gchar *text, Window *window){ + if(!window || barName >= window->statusBlocks) return; + gtk_entry_set_text(window->SBars[barName], text); +} + +/* + * This three short functions need to have ability to change scale + * of axe Y from menu + */ +gboolean redraw_D(Window *window){ + Gconfigure(window); + gdk_window_invalidate_rect(window->drawingArea->window, NULL, FALSE); + refresh_state(window); + return TRUE; +} +gboolean log_scale(Window *window){ // press 'e' in graph window + window->context->graphYaxis |= + (window->context->graphMode == GR_GRAPH) ? Y_LOGGRAPH : Y_LOGHIST; + redraw_D(window); + return TRUE; +} +gboolean lin_scale(Window *window){ // press 'l' in graph window + window->context->graphYaxis &= (window->context->graphMode == GR_GRAPH) ? + ~Y_LOGGRAPH : ~Y_LOGHIST; + redraw_D(window); + return TRUE; +} + +// Show histogram in chart window +void show_histogram(Window *window){ + IMAGE *ima; + if(!window->context->current_image || !window->context->current_image->data){ + window->context->current_image = window->image; + if(!window->image || !window->image->data) return; + } + ima = window->context->current_image; + if(!ima->stat.histogram.data) + if(!make_histogram(ima)) + return; + gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(window->graphWindow->LinLogMenu[isYlog(window)]), TRUE); + gtk_widget_show_all(window->graphWindow->window); + do_histogram(window); +} + +void correct_zoom_by_move(Window *window, double Zoom_factor, double *x, double *y){ + double xx, yy, Xc, Yc, ZZ; + if(!x) xx = (double)window->drawingArea->allocation.width / 2.; + else xx = *x; + if(!y) yy = (double)window->drawingArea->allocation.height / 2.; + else yy = *y; + //DBG("x=%g, y=%g",xx,yy); + conv_mouse_to_image_coords(xx, yy, &Xc, &Yc, window); + Xc -= window->texture->w / 2.; Yc -= window->texture->h / 2.; + //DBG("Xc=%g, Yc=%g, ZF=%g",Xc, Yc, Zoom_factor); + // It's amazing (if U don't know what float is), but Zoom*(Zoom_factor - 1) isn't the same + ZZ = window->Zoom*Zoom_factor - window->Zoom; + window->move.x -= Xc * ZZ; + window->move.y -= Yc * ZZ; + //DBG("moveX=%g, moveY=%g\n", window->move.x, window->move.y); + window->Zoom *= Zoom_factor; +} + +void zoom_near(Window *window){ // run for '+' key + correct_zoom_by_move(window, ZOOM_NEAR, NULL, NULL); + force_redraw(window->drawingArea); +} +void zoom_far(Window *window){ // run for '-' key + correct_zoom_by_move(window, ZOOM_FAR, NULL, NULL); + force_redraw(window->drawingArea); +} + +void zoom_selection(Window *window){ // function called after frame selection + double x0,y0, x,y, zX, zY, ZF; + get_sel_region(&x0, &y0, &x, &y); + zX = (double)window->drawingArea->allocation.width / abs(x-x0); + zY = (double)window->drawingArea->allocation.height / abs(y-y0); + ZF = (zX < zY) ? zX : zY; + x = (x + x0) / 2.; + y = (y + y0) / 2.; + correct_zoom_by_move(window, ZF, &x, &y); + force_redraw(window->drawingArea); +} + +void show_3D_terrain(Window *window){ + double imX0, imY0, imX, imY; + double x0,y0, x,y; + get_sel_region(&imX0, &imY0, &imX, &imY); + conv_mouse_to_image_coords(imX0, imY0, &x0, &y0, window); + conv_mouse_to_image_coords(imX, imY, &x, &y, window); + Window *terra = init_window(window, GL3D_WINDOW); + terrain_3D(terra, x0,y0, x,y, FALSE); +} + +void full_3D_terrain(Window *window){ + Window *terra = init_window(window, GL3D_WINDOW); + terrain_3D(terra, 0,0, window->image->width,window->image->height, FALSE); +} + +gboolean is_tracking = FALSE; // TRUE is cutting mode +gboolean zoom_frame = FALSE; // TRUE is zoom frame mode +gboolean select_3D = FALSE; // TRUE is 3D frame selection mode +/* + * Processing of mouse button press event + * 1. Main window + */ +gboolean press_main_mbtn(Window *window, GdkEventButton *event){ + Context *c = window->context; + switch(event->button){ + case 1: // Left button? Start cutting + // Redraw window to delete old garbage + if(!window->image->data) break; + switch(c->drawingMode){ + case DRAW_TRACK: + gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(window->graphWindow->LinLogMenu[isYlog(window)]), TRUE); + gtk_widget_show_all(window->graphWindow->window); + c->graphMode = GR_GRAPH; + if(event->state & GDK_CONTROL_MASK) + c->trackingMode = TRACK_DIAG; + else if(event->state & GDK_SHIFT_MASK) + c->trackingMode = TRACK_VERT; + else if(event->state & GDK_MOD1_MASK) + c->trackingMode = TRACK_HORIZ; + else + c->trackingMode = TRACK_ANY; + case DRAW_QUAD: // There's no break here and it's not an error! + is_tracking = TRUE; + do_tracking(event->x, event->y, TRUE, window); + break; + case DRAW_SELECTION: + if(window->image->imagetype == HOUGH){ + double R, phi; + get_houg_line(window, event->x, event->y, &phi, &R); + gchar *text = g_strdup_printf("%s: R=%.0f, phi=%.1f", + _("Selected line"), R, phi); + set_status_text(StatusText, text, window); + g_free(text); + } + else + pickRegion(event->x, event->y); + break; + } + gdk_window_invalidate_rect(window->drawingArea->window, + &window->drawingArea->allocation, FALSE); + break; + case 2: // Middle button moves an picture + X0 = event->x; Y0 = event->y; + lX0 = window->move.x; lY0 = window->move.y; + break; + case 3: + break; + } + refresh_state(window); + return FALSE; +} +// 2. Auxiliary window +gboolean press_sub_mbtn(Window *window, GdkEventButton * event){ + switch(event->button){ + case 1: + is_tracking = graph_mouse_btn((int)event->x, window); + break; + case 2: + break; + case 3: + break; + } + refresh_state(window); + return FALSE; +} + +/* + * Reaction for button release + * 1. Main window + */ +gboolean release_main_mbtn(Window *window, GdkEventButton * event){ + switch(event->button){ + case 1: + // there was something selected by frame + if(is_tracking && window->context->drawingMode == DRAW_QUAD){ + if(zoom_frame){ // It was a zoom selection + zoom_selection(window); + }else if(select_3D){ + show_3D_terrain(window); + } + } + break; + case 2: + break; + case 3: + break; + } + return FALSE; +} + +// 2. Auxiliary window +gboolean release_sub_mbtn(Window *window, GdkEventButton * event){ + switch(event->button){ + case 1: + switch(window->context->graphMode){ + case GR_GRAPH: + break; + case GR_HISTOGRAM: + gen_tres_texture(window->parent); + break; + } + break; + } + return FALSE; +} + +/* + * Functions of menus processing + */ +void show_hist(Window *window){ // 'h' in main window + window->context->graphMode = GR_HISTOGRAM; + show_histogram(window); +} +void spots_coords(Window *window){ // 'c' + get_spots_coords(window, window->image_transformed); +} + +void set_spots_parameters(Window *window){ // 'alt+s' + GtkBuilder *builder; + GError *err = NULL; + GtkWidget *dialog, *btn; + void getval(GtkSpinButton *spin_button, int *val){ + *val = gtk_spin_button_get_value_as_int(spin_button); + DBG("val = %d", *val); + } + void chk_tres_vals(GtkWidget *wnd){ + GtkMessageDialog *dialog; + gchar *text; + gboolean W, H; + H = (Global->maxSpotH > Global->minSpotH); + W = (Global->maxSpotW > Global->minSpotW); + if(H && W){ // OK + gtk_widget_destroy(wnd); + return; + } + if(!(H || W)) + text = _("Both max values are less than min"); + else{ + if(!H) + text = _("Max height value less than min"); + else + text = _("Max width value less than min"); + } + dialog = GTK_MESSAGE_DIALOG(gtk_message_dialog_new(NULL, + GTK_DIALOG_MODAL, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_CLOSE, + text)); + gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(wnd)); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(GTK_WIDGET(dialog)); + } + void conn_sig(gchar *object, int *val){ + GtkAdjustment *adj; + GtkWidget *spin = GTK_WIDGET(gtk_builder_get_object(builder, object)); + g_signal_connect(spin, "value-changed", G_CALLBACK(getval), val); + adj = (GtkAdjustment*) gtk_adjustment_new(*val, 1.0, 4000.0, 1.0, 0.0, 0.0); + gtk_spin_button_set_adjustment(GTK_SPIN_BUTTON(spin), adj); + } + builder = gtk_builder_new(); + if(!gtk_builder_add_from_string(builder, ui, -1, &err)){ + g_warning("%d, %s\n", err->code, err->message); + exit(-1); + } + dialog = GTK_WIDGET(gtk_builder_get_object(builder, "TresWindow")); + conn_sig("MinWidth", &Global->minSpotW); + conn_sig("MaxWidth", &Global->maxSpotW); + conn_sig("MinHeight", &Global->minSpotH); + conn_sig("MaxHeight", &Global->maxSpotH); + btn = GTK_WIDGET(gtk_builder_get_object(builder, "TresCancel")); + g_signal_connect_swapped(btn, "clicked", + G_CALLBACK(gtk_widget_destroy), dialog); + btn = GTK_WIDGET(gtk_builder_get_object(builder, "TresOK")); + g_signal_connect_swapped(btn, "clicked", + G_CALLBACK(chk_tres_vals), dialog); + run_modal_window(GTK_WINDOW(dialog), window); +} + +void circle_params(Window *window){ // 'x' + get_circles_params(window, window->image_transformed); +} +void spot_ident(Window *window){ // 's' + window->context->visualMode |= SHOW_POTSELECT; + window->context->drawingMode = DRAW_SELECTION; + force_redraw(window->drawingArea); +} +void save_spots(Window *window){ + if(!window->spots){ + g_err(_("No recognized spots")); + return; + } + GtkFileChooser *dialog; + gchar *filename = NULL, *tmp = NULL; + gint response; + dialog = GTK_FILE_CHOOSER( + gtk_file_chooser_dialog_new("Save File", NULL, + GTK_FILE_CHOOSER_ACTION_SAVE, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT, + NULL)); + if(saved_path) + gtk_file_chooser_set_current_folder(dialog, saved_path); + filename = g_strdup_printf("%s", window->image->filename); + DBG("saved path: %s; filename: %s", saved_path, filename); + if( (tmp = strrchr(filename, '.')) ) *tmp = 0; + tmp = g_strdup_printf("%s.spotlist", filename); + gtk_file_chooser_set_current_name(dialog, tmp); + g_free(tmp); + g_free(filename); filename = NULL; + gtk_file_chooser_set_do_overwrite_confirmation(dialog, TRUE); + response = run_modal_dialog(GTK_DIALOG(dialog), window); + if(response == GTK_RESPONSE_ACCEPT){ + filename = gtk_file_chooser_get_filename(dialog); + } + gtk_widget_destroy(GTK_WIDGET(dialog)); + if(!filename) return; + spots_save(window->spots, filename); + g_print("%s saved", filename); + tmp = g_path_get_basename(filename); + g_free(filename); + filename = g_strdup_printf("%s saved", tmp); + set_status_text(StatusText, filename, window); + g_free(filename); g_free(tmp); +} +void draw_tracks(Window *window){ // 't' + window->context->drawingMode = DRAW_TRACK; + window->context->visualMode &= ~SHOW_POTSELECT; +} +void zoom_restore(Window *window){ // '0' + window->Zoom = 1.; + window->move.x = 0.; + window->move.y = 0.; + window->Zangle = 0.; + window->Xangle = 0.; + force_redraw(window->drawingArea); +} +void zooom_qframe(Window *window){ // 'z' + window->context->drawingMode = DRAW_QUAD; + zoom_frame = TRUE; +} +void select_terrain(Window *window){ // '3' + window->context->drawingMode = DRAW_QUAD; + select_3D = TRUE; +} +void filter(Window *window){ // 'f' +/* Filter *f; + INIT(f, Filter);*/ +/* f->FilterType = MEDIAN; + f->sx=f->sy=9.; + f->w = f->h = 3; + filter_image(window, f);*/ +/* f->FilterType = SIMPLEGRAD; + filter_image(window, f); +*/ + find_contours(window, 16, LOG); +/* f->FilterType = STEP; + f->w = 16; + f->h = LOG; + filter_image(window, f);*/ + /*f->FilterType = LAPGAUSS; + f->w = f->h = 256; + f->sx = f->sy = 5.f; + filter_image(window, f);*/ +} + +/* + * Processing of a key pressing events + * 1. Main window + */ +gboolean press_main_key(Window *window, GdkEventKey * event){ + //FNAME(); + gboolean ret = TRUE; + if(event->keyval == GDK_Escape){ + if(zoom_frame || select_3D){ + window->context->drawingMode = DRAW_TRACK; + zoom_frame = FALSE; // ESC cansels zoom + select_3D = FALSE; + } + return TRUE; + } + if(event->state & (GDK_CONTROL_MASK)){ // with ctrl + switch(event->keyval){ + default: ret = FALSE; + } + } + else{ + switch(event->keyval){ + case GDK_Page_Up: case GDK_Up: case GDK_Left: + if(window->files.list_length > 1) + change_image(get_filename(PREVIOUS, window), window); + break; + case GDK_Page_Down: case GDK_Down: case GDK_Right: + if(window->files.list_length > 1) + change_image(get_filename(NEXT, window), window); + break; + case GDK_Home: + if(window->files.list_length > 1) + change_image(get_filename(FIRST, window), window); + break; + case GDK_End: + if(window->files.list_length > 1) + change_image(get_filename(LAST, window), window); + break; + default: ret = FALSE; + } + } + //refresh_state(window); + return ret; +} +// 2. Auxiliary window +/* +gboolean press_sub_key(Window *window, GdkEventKey * event){ + //FNAME(); + gboolean ret = TRUE; + switch(event->keyval){ + default: ret = FALSE; + } + return ret; +} +*/ + +/* +gboolean release_key(GtkWidget * none, GdkEventKey * event){ + //DBG("Release key %d\n", event->keyval); + return FALSE; +}*/ + +// Scroll +gboolean main_scroll(Window *window, GdkEventScroll *event){ + gboolean ret = FALSE; + if(event->state & (GDK_CONTROL_MASK)){ + switch(event->direction){ + case GDK_SCROLL_UP: + zoom_far(window); + ret = TRUE; + break; + case GDK_SCROLL_DOWN: + zoom_near(window); + ret = TRUE; + break; + default: break; + } + } + return ret; +} + +/* + * Mouse move events + * 1. Main window + */ +gboolean main_motion(Window *window, GdkEventMotion * event){ + double x,y; + gchar buf[40]; + conv_mouse_to_image_coords(event->x, event->y, &x, &y, window); + g_snprintf(buf, 39, "(%.1f, %.1f)", x, y); + set_status_text(StatusCoords, buf, window); + if(event->state & GDK_BUTTON1_MASK){ + switch(window->context->drawingMode){ + case DRAW_TRACK: // In tracking mode show track + case DRAW_QUAD: // the same in frame selection mode + if(is_tracking) do_tracking(event->x, event->y, FALSE, window); + break; + } + } + else if(event->state & GDK_BUTTON2_MASK){ + window->move.x = lX0 + (event->x - X0) * window->Daspect; + window->move.y = lY0 + (Y0 - event->y) * window->Daspect; + force_redraw(window->drawingArea); + } + else if(event->state & GDK_BUTTON3_MASK){ + } + return TRUE; +} + +// 2. Auxiliary window +gboolean sub_motion(Window *window, GdkEventMotion * event){ + double x = event->x / xScale; + double y = gYmax - event->y/yScale; + double xx, yy; + gchar buf[40]; + get_image_crds(x, &xx, &yy); + if(isYlog(window)) y = exp(y); + g_snprintf(buf, 39, "(%.3f, %.3f) I(%.1f, %.1f)", x, y, xx, yy); + set_status_text(StatusCoords, buf, window); + if(event->state & GDK_BUTTON1_MASK){ + if(is_tracking) graph_mouse_btn((int)event->x, window); + } + return TRUE; +} + +// 3. OpenGL window +void open3D(Window *window){ + terrain_3D(window, 0.,0.,0.,0., TRUE); // Show open dialog & load new picture +} +void rotXp(Window *window){ + window->Xangle += key_acceleration; + if(window->Xangle > 360.) window->Xangle -= 360.; + force_redraw(window->drawingArea); +} +void rotXm(Window *window){ + window->Xangle -= key_acceleration; + if(window->Xangle < 0.) window->Xangle += 360.; + force_redraw(window->drawingArea); +} +void rotZp(Window *window){ + window->Zangle += key_acceleration; + if(window->Zangle > 360.) window->Zangle -= 360.; + force_redraw(window->drawingArea); +} +void rotZm(Window *window){ + window->Zangle -= key_acceleration; + if(window->Zangle < 0.) window->Zangle += 360.; + force_redraw(window->drawingArea); +} +void moveXp(Window *window){ + window->move.x += key_acceleration; + force_redraw(window->drawingArea); +} +void moveXm(Window *window){ + window->move.x -= key_acceleration; + force_redraw(window->drawingArea); +} +void moveYp(Window *window){ + window->move.y += key_acceleration; + force_redraw(window->drawingArea); +} +void moveYm(Window *window){ + window->move.y -= key_acceleration; + force_redraw(window->drawingArea); +} +void game_move(Window *window, double where){ + double lx, ly, lz, za = window->Zangle, xa = window->Xangle; + lx = cos(za)*sin(xa); + ly = -sin(za); + lz = -cos(za)*cos(xa); + double delta = where * key_acceleration; + window->move.x += delta*lx; + window->move.y += delta*ly; + window->Zoom += delta*lz/window->image->height; +} +void moveZp(Window *window){ + if(window->context->drawingMode == GAME_MODE) + game_move(window, 1.); + else + window->Zoom *= 1.1; + force_redraw(window->drawingArea); +} +void moveZm(Window *window){ + if(window->context->drawingMode == GAME_MODE) + game_move(window, -1.); + else + window->Zoom /= 1.1; + force_redraw(window->drawingArea); +} +void game_mode(Window *window){ + if(window->context->drawingMode == GAME_MODE){ + window->context->drawingMode = DRAW_TRACK; + gdk_pointer_ungrab(GDK_CURRENT_TIME); + gtk_window_unfullscreen(GTK_WINDOW(window->window)); + gdk_window_set_cursor(GDK_WINDOW(window->drawingArea->window), NULL); + } + else{ + window->context->drawingMode = GAME_MODE; + gdk_pointer_grab(window->drawingArea->window, TRUE, 0, + window->drawingArea->window, NULL, + GDK_CURRENT_TIME); + gtk_window_fullscreen(GTK_WINDOW(window->window)); + GdkCursor *cursor = gdk_cursor_new(GDK_BLANK_CURSOR); + gdk_window_set_cursor(GDK_WINDOW(window->drawingArea->window), cursor); + } + zoom_restore(window); +} +gfloat oldXbtn, oldYbtn; +gint oldXscreen, oldYscreen; +gboolean CursorWasMoved = TRUE; +gboolean press_ogl_mbtn(Window *window, GdkEventButton *event){ + if(window->context->drawingMode == GAME_MODE && + event->button == 1){ + GdkDisplay *disp = gdk_display_get_default(); + GdkScreen* screen = gdk_display_get_default_screen(disp); + oldXscreen = gdk_screen_get_width(screen) / 2; + oldYscreen = gdk_screen_get_height(screen) / 2; + gdk_display_warp_pointer(disp, screen, + oldXscreen, oldYscreen); + CursorWasMoved = TRUE; + } + return FALSE; +} +gboolean release_ogl_mbtn(Window *window __attribute__((unused)), + GdkEventButton *event __attribute__((unused))){ + return FALSE; +} +/* + * Move mouse in 3D mode + * !!! In game mode angles measures in radians, not degrees !!! + */ +gboolean ogl_motion(Window *window, GdkEventMotion * event){ + if(window->context->drawingMode == GAME_MODE + && event->state & GDK_BUTTON1_MASK){ + if(CursorWasMoved){ + CursorWasMoved = FALSE; + oldXbtn = event->x; + oldYbtn = event->y; + return FALSE; + } + window->Xangle += (event->x - oldXbtn)/1000.; + window->Zangle += (event->y - oldYbtn)/1000.; + GdkDisplay *disp = gdk_display_get_default(); + gdk_display_warp_pointer(disp, gdk_display_get_default_screen(disp), + oldXscreen, oldYscreen); + CursorWasMoved = TRUE; + force_redraw(window->drawingArea); + } + return FALSE; +} +gboolean reset_accel(){ + key_acceleration = 1.; + return FALSE; +} +gboolean press_ogl_key(Window *window, GdkEventKey * event){ + static gint tag = 0; + if(tag) gtk_timeout_remove(tag); + tag = gtk_timeout_add(100, reset_accel, NULL); + key_acceleration += 1.; + switch(event->keyval){ + case GDK_Left: + moveXp(window); // Move left - picture moves to the right + break; + case GDK_Right: + moveXm(window); + break; + case GDK_Up: + moveZp(window); + break; + case GDK_Down: + moveZm(window); + break; + default: + return FALSE; + } + return TRUE; +} +gboolean release_ogl_key(Window *window __attribute__((unused)), + GdkEventKey *event __attribute__((unused))){ + return FALSE; +} +gboolean ogl_scroll(Window *window __attribute__((unused)), + GdkEventScroll *event __attribute__((unused))){ + return FALSE; +} + +/* + * Redefine rulers limits, x0 & xm are the limits of horizontal ruler + * y0 & ym - vertical + * hrule, vrule - appropriate rulers + */ +void set_rules(double x0, double y0, double xm, double ym, + GtkWidget *hrule, GtkWidget *vrule){ + double yy = (y0 < 0.) ? 0 : y0; + gtk_ruler_set_range(GTK_RULER (hrule), x0, xm, 0., xm); + gtk_ruler_set_range(GTK_RULER (vrule), y0, ym, yy, ym); +} +void set_Drulers(double x0, double y0, double xm, double ym, Window *window){ + set_rules(-x0, ym, xm, -y0, window->hRule, window->vRule); +} +void set_Grulers(double y0, double xm, double ym, Window *window){ + //DBG("gXm=%g, gY0=%g, gYm=%g\n", xm, y0, ym); + set_rules(0., ym, xm, y0, window->hRule, window->vRule); +} + +void destroy_window(Window* window){ + FNAME(); + if(!window) return; + if(window->id == GRAPH_WINDOW) + window->parent->graphWindow = NULL; + else{ + destroy_window(window->graphWindow); + _FREE(window->context); + } + if(window->id == GL3D_WINDOW) + freeGLmemory(window); + if(window->image){ + if(window->id == GL3D_WINDOW){ + window->image->cLevels = NULL; + } + destroy_image(window->image); + } + if(window->image_transformed) + destroy_image(window->image_transformed); + _FREE(window->texture); + spots_free(&window->spots); + free_filelist(window); + gtk_widget_destroy(window->window); + free(window); +} + +/* + * Signal handler structure definition + * array Sighandler[] should end by NULL element + */ +typedef struct{ + gchar *elementName; // name of element + gchar *sigName; // signal name + GCallback sigHandler; // function - handler, a kind of void f(Window* w) +} Sighandler; +// Signals for graph window +Sighandler graphSigHandler[] = { + {"graphCloseMenuItem", "activate", G_CALLBACK(close_subwindow)}, + {"graphGaussMenuItem", "activate", G_CALLBACK(fit_gaussian)}, + {"graphLinMenuItem", "activate", G_CALLBACK(lin_scale)}, + {"graphLogMenuItem", "activate", G_CALLBACK(log_scale)}, + {"graphArea", "expose-event", G_CALLBACK(Gexpose)}, + {"graphArea", "configure-event", G_CALLBACK(Gconfigure)}, + {NULL, "button-press-event", G_CALLBACK(press_sub_mbtn)}, + {NULL, "button-release-event", G_CALLBACK(release_sub_mbtn)}, + {NULL, "motion-notify-event", G_CALLBACK(sub_motion)}, +// {NULL, "key-press-event", G_CALLBACK(press_sub_key)}, + {NULL, NULL, NULL} +}; +// Signals for window with picture +Sighandler mainSigHandler[] = { + {"mainOpenMenuItem", "activate", G_CALLBACK(open_file)}, + {"mainSaveAsMenuItem", "activate", G_CALLBACK(save_as)}, + {"mainOpenInNewMenuItem", "activate", G_CALLBACK(open_in_new)}, + {"mainOpenIn3DMenuItem", "activate", G_CALLBACK(open_in_3D)}, + {"mainHistMenuItem", "activate", G_CALLBACK(show_hist)}, + {"mainHeadersMenuItem", "activate", G_CALLBACK(edit_headers)}, + {"mainSpotsidentMenuItem", "activate", G_CALLBACK(spot_ident)}, + {"mainSaveSpotsMenuItem", "activate", G_CALLBACK(save_spots)}, + {"mainTrackMenuItem", "activate", G_CALLBACK(draw_tracks)}, + {"main3DviewFrameMenuItem", "activate", G_CALLBACK(select_terrain)}, + {"main3DviewFullMenuItem", "activate", G_CALLBACK(full_3D_terrain)}, + {"mainZoomframeMenuItem", "activate", G_CALLBACK(zooom_qframe)}, + {"mainZoomrestoreMenuItem", "activate", G_CALLBACK(zoom_restore)}, + {"mainZoomnearMenuItem", "activate", G_CALLBACK(zoom_near)}, + {"mainZoomfarMenuItem", "activate", G_CALLBACK(zoom_far)}, + {"mainSpotsParamMenuItem", "activate", G_CALLBACK(set_spots_parameters)}, + {"mainFindSpotsMenuItem", "activate", G_CALLBACK(spots_coords)}, + {"mainHartmannMenuItem", "activate", G_CALLBACK(hartmann_spots)}, + {"mainCirclesMenuItem", "activate", G_CALLBACK(circle_params)}, + {"mainHoughMenuItem", "activate", G_CALLBACK(hough_lines)}, + {"mainFilterMenuItem", "activate", G_CALLBACK(filter)}, +// {"", "activate", G_CALLBACK()}, + {NULL, "button-press-event", G_CALLBACK(press_main_mbtn)}, + {NULL, "button-release-event", G_CALLBACK(release_main_mbtn)}, + {NULL, "motion-notify-event", G_CALLBACK(main_motion)}, + {NULL, "key-press-event", G_CALLBACK(press_main_key)}, + {NULL, "scroll-event", G_CALLBACK(main_scroll)}, + {NULL, NULL, NULL} +}; +// Signals for 3D window +Sighandler oglSigHandler[] = { + {"oglOpenMenuItem", "activate", G_CALLBACK(open3D)}, + {"oglCloseMenuItem", "activate", G_CALLBACK(destroy_window)}, + {"oglZoomrestoreMenuItem", "activate", G_CALLBACK(zoom_restore)}, + {"oglXAplusMenuItem", "activate", G_CALLBACK(rotXp)}, + {"oglXAminusMenuItem", "activate", G_CALLBACK(rotXm)}, + {"oglZAplusMenuItem", "activate", G_CALLBACK(rotZp)}, + {"oglZAminusMenuItem", "activate", G_CALLBACK(rotZm)}, + {"oglXplusMenuItem", "activate", G_CALLBACK(moveXp)}, + {"oglXminusMenuItem", "activate", G_CALLBACK(moveXm)}, + {"oglYplusMenuItem", "activate", G_CALLBACK(moveYp)}, + {"oglYminusMenuItem", "activate", G_CALLBACK(moveYm)}, + {"oglZplusMenuItem", "activate", G_CALLBACK(moveZp)}, + {"oglZminusMenuItem", "activate", G_CALLBACK(moveZm)}, + {"oglGameModeMenuItem", "toggled", G_CALLBACK(game_mode)}, + {NULL, "button-press-event", G_CALLBACK(press_ogl_mbtn)}, + {NULL, "button-release-event", G_CALLBACK(release_ogl_mbtn)}, + {NULL, "motion-notify-event", G_CALLBACK(ogl_motion)}, + {NULL, "key-press-event", G_CALLBACK(press_ogl_key)}, + {NULL, "key-release-event", G_CALLBACK(release_ogl_key)}, + {NULL, "scroll-event", G_CALLBACK(ogl_scroll)}, + {NULL, NULL, NULL} +}; + +inline GtkWidget *initA(gchar *prefix, GtkBuilder *builder){ + gchar text[32]; + g_sprintf(text, "%sArea", prefix); + GtkWidget *A = GTK_WIDGET(gtk_builder_get_object(builder, text)); + gtk_widget_set_events(A, GDK_EXPOSURE_MASK | + GDK_BUTTON_PRESS_MASK | + GDK_POINTER_MOTION_MASK | + GDK_POINTER_MOTION_HINT_MASK| + GDK_BUTTON_RELEASE_MASK); + return A; +} +inline GtkWidget *initR(GtkWidget *area, gchar *name, GtkBuilder *builder){ + GtkWidget *R = GTK_WIDGET(gtk_builder_get_object(builder, name)); + g_signal_connect_swapped(area, "motion-notify-event", + G_CALLBACK(EVENT_METHOD(R, motion_notify_event)), R); + return R; +} + +/* + * Window initialisation + * parent - parent window + * handlers - array of signal handlers + * prefix - window name prefix (main, graph, ...) + * nBlocks - amount of blocks in status bar + */ +Window *init_window(Window *parent, int winId){ + FNAME(); + guint i; + GtkBuilder *builder; + Window *window; + Sighandler *handlers = NULL; + gchar *prefix = NULL, *title = NULL; + guint nBlocks = 0; + GError *err = NULL; + gchar text[32]; + builder = gtk_builder_new(); + if(!gtk_builder_add_from_string(builder, ui, -1, &err)){ + g_warning("%d, %s\n", err->code, err->message); + exit(-1); + } + INIT(window, Window); + window->id = winId; + if(parent){ + window->parent = parent; + if(winId == GRAPH_WINDOW) // if window is graph context equal to parent's + window->context = parent->context; + else{ + window->context = COPY(parent->context, Context); + window->context->visualMode = SHOW_ONLY_IMAGE; + } + }else + INIT(window->context, Context); + switch(winId){ + case MAIN_WINDOW: + case OPENGL_WINDOW: + prefix = "main"; + handlers = mainSigHandler; + nBlocks = 4; + break; + case GRAPH_WINDOW: + prefix = "graph"; + handlers = graphSigHandler; + nBlocks = 3; + break; + case GL3D_WINDOW: + prefix = "ogl"; + handlers = oglSigHandler; + break; + } + g_sprintf(text, "%sWindow", prefix); + window->window = GTK_WIDGET(gtk_builder_get_object(builder, text)); + if(winId == MAIN_WINDOW || winId == OPENGL_WINDOW){ + GtkAction *quit = GTK_ACTION(gtk_builder_get_object(builder, "mainQuitMenuItem")); + gtk_action_disconnect_accelerator(quit); + if(winId == MAIN_WINDOW){ + g_signal_connect(quit, "activate", chk_quit, NULL); + gtk_action_set_icon_name(quit, "application-exit"); + gtk_action_set_accel_path(quit, "
/quit"); + gtk_action_set_label(quit, _("Quit")); + }else{ + gtk_action_set_accel_path(quit, "
/close"); + gtk_action_set_label(quit, _("Close")); + g_signal_connect_swapped(quit, "activate", + G_CALLBACK(destroy_window), window); + gtk_action_set_icon_name (quit, "window-close"); + } + gtk_action_connect_accelerator(quit); + } + window->drawingArea = initA(prefix, builder); + if(winId != GL3D_WINDOW){ + g_sprintf(text, "%sHruler", prefix); + window->hRule = initR(window->drawingArea, text, builder); + g_sprintf(text, "%sVruler", prefix); + window->vRule = initR(window->drawingArea, text, builder); + } + for(i = 0; i < nBlocks; i++){ + g_sprintf(text, "%sStatusBar%d", prefix, i); + window->SBars[i] = GTK_ENTRY(gtk_builder_get_object(builder, text)); + #ifdef EBUG + set_status_text(i, text, window); + #endif + } + window->statusBlocks = nBlocks; + if(winId != GRAPH_WINDOW){ + INIT(window->image, IMAGE); + INIT(window->image_transformed, IMAGE); + initGl(window->drawingArea); + }else{ + window->LinLogMenu[0] = GTK_RADIO_ACTION(gtk_builder_get_object(builder, "graphLinMenuItem")); + window->LinLogMenu[1] = GTK_RADIO_ACTION(gtk_builder_get_object(builder, "graphLogMenuItem")); + gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(window->LinLogMenu[0]), TRUE); + } + while(handlers->sigHandler){ + void *element; + if(handlers->elementName) + element = (void*)gtk_builder_get_object(builder, handlers->elementName); + else + element = (void*)window->window; + g_signal_connect_swapped(element, handlers->sigName, + handlers->sigHandler, window); + handlers++; + } + gtk_builder_connect_signals(builder, NULL); + g_object_unref(G_OBJECT(builder)); + if(winId != GRAPH_WINDOW){ + if(winId == MAIN_WINDOW || winId == OPENGL_WINDOW) + window->graphWindow = init_window(window, GRAPH_WINDOW); + gtk_widget_show_all(window->window); + } + window->Daspect = 1.; window->Zoom = 1.; + if(winId != OPENGL_WINDOW) + title = g_strdup_printf("%s (%s)", PRGNAME, prefix); + else + title = g_strdup_printf("%s", PRGNAME); + gtk_window_set_title(GTK_WINDOW(window->window), title); + g_free(title); + return window; +} + +void init_main_window(int *argc, char ***argv){ + gtk_init(argc, argv); + gtk_gl_init(argc, argv); + gtk_accel_map_add_entry("
/close", GDK_w, GDK_CONTROL_MASK); + gtk_accel_map_add_entry("
/quit", GDK_q, GDK_CONTROL_MASK); + mainWindow = init_window(NULL, MAIN_WINDOW); +} + +/* + * *prefocal = TRUE if image is prefocal, FALSE otherwisw + * (Ask user, this function calls only if there's a problems to find VAL_F in FITS keys) + */ +void get_prefocal(Window *window, gboolean *prefocal){ + double foc; + GtkWidget *dialog, *btn, *focus, *radio; + void ch_val_f(GtkEntry *e){ + char *eptr = NULL; + errno = 0; + foc = strtod(gtk_entry_get_text(e), &eptr); + if((eptr && *eptr) || errno == ERANGE){ + gchar *txt = g_strdup_printf("%g", foc); + gtk_entry_set_text(e, txt); + g_free(txt); + } + DBG("F: %g", foc); + } + void ch_vals(GtkWidget *w){ + *prefocal = !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(radio)); + DBG("prefocal: %d\n", *prefocal); + window->image->val_f = foc; + gtk_widget_destroy(w); + } + GtkBuilder *builder; + GError *err = NULL; + gchar *text; + if(!window || !window->image) return; + if(window->image->val_f > 0.) text = g_strdup_printf("%.3f", window->image->val_f); + else text = g_strdup_printf("0.000"); + builder = gtk_builder_new(); + if(!gtk_builder_add_from_string(builder, ui, -1, &err)){ + g_warning("%d, %s\n", err->code, err->message); + exit(-1); + } + dialog = GTK_WIDGET(gtk_builder_get_object(builder, "FocalWindow")); + radio = GTK_WIDGET(gtk_builder_get_object(builder, "FocalPostRadioBtn")); + focus = GTK_WIDGET(gtk_builder_get_object(builder, "FocFocusEntry")); + g_signal_connect(focus, "changed", G_CALLBACK(ch_val_f), NULL); + gtk_entry_set_text(GTK_ENTRY(focus), text); + btn = GTK_WIDGET(gtk_builder_get_object(builder, "FocalOK")); + g_signal_connect_swapped(btn, "clicked", + G_CALLBACK(ch_vals), dialog); + run_modal_dialog(GTK_DIALOG(dialog), window); + g_free(text); +} diff --git a/src/imtools.c b/src/imtools.c new file mode 100644 index 0000000..436595b --- /dev/null +++ b/src/imtools.c @@ -0,0 +1,638 @@ +// imtools.c - different image transforms +// +// Copyright 2011 Edward V. Emelianoff +// +// This program 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 2 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 this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +// MA 02110-1301, USA. +#include "fitsheaders.h" +#include "imtools.h" +#include "contours.h" +#include "opengl.h" +#include "gauss.h" +#include "CUtools.h" + +// Histogram size, 256 by default +const int HIST_SIZE = 256; +const int MIN_SPOT_NO = 200; // minimum amount of spots on a hartmanogram +const int MAX_SPOT_NO = 258; // max amount -//- + +void destroy_image(IMAGE *image){ + if(!image) return; + _FREE(image->data); + g_free(image->filename); + if(image->store) // clear parameters from FITS header + gtk_list_store_clear(image->store); + _FREE(image->cLevels); // isolines' intensity + free_contours(image); // isolines itself + _FREE(image); +} + +#ifdef LEPTONICA_FOUND +/* + * Convert an image ima (after tresholding) + * into a pixmap for pixConnComp from leptonica + */ +PIX *conv_image2pix(IMAGE *ima){ + PIX *pixs; + l_uint32 byte, *pptr; + GLfloat *iptr, *idata; + int w, h, wpl, i, j, k, N; + w = ima->width; + h = ima->height; + pixs = pixCreate(w, h, 1); + wpl = pixGetWpl(pixs); + pptr = pixGetData(pixs); + idata = ima->data; + for(i = h - 1; i > -1; i--){ // lines cycle + N = 0; + iptr = &idata[i * w]; + for(j = 0; j < wpl; j++){ // cycle by 32 columns + byte = 0; + for(k = 0; k < 32; k++){ // cycle by a 32-bit word bits + byte <<= 1; + // if string still doesn't end & intensity higher 0.1% + if(N++ < w && *iptr++ > 0.001) + byte |= 1; // set lesser bit + } + *pptr++ = byte; + } + } + return pixs; +} + +gboolean check_box(Window *window, BOX *box, int *X0, int *Y0, int *X, int *Y){ + if(!window->image->data) return FALSE; + int WD = window->image->width, HT = window->image->height - 1; + if(!(box && X0 && Y0 && X && Y)) return FALSE; + *X0 = box->x; + *Y0 = box->y; + *X = *X0 + box->w; + *Y = *Y0 + box->h; // coordinates of a box corners + if(box->w < 5 || box->h < 5) return FALSE; + if(*X0 < 0 || *X0 > WD || *X < 0 || *X > WD || + *Y0 < 0 || *Y0 > HT || *Y < 0 || *Y > HT) + return FALSE; + return TRUE; +} + + //Calculation of min & max intensity by picture to show it +void compute_minmax(IMAGE *ima){ + float min, max, *dst = ima->data; + int i, j, w=ima->width, h=ima->height; + max = min = *dst; + for(i=0; i max) max = tmp; + else if(tmp < min) min = tmp; + } + ima->stat.max = max; + ima->stat.min = min; + //ima->width = w; ima->height = h; + DBG("stat: max=%.2f, min=%.2f", max, min); +} + +/* + * Calculation of the center of box for image ima + * by inscribing a Gaussian to a vector obtained + * by addition of box columns (for X) and rows (for Y) + */ +gboolean get_gaussian_center(Window *window, BOX *box, Coordinates *crds){ + int WD = window->image->width, HT = window->image->height - 1; + int x, y; + GLfloat *data; + Points row, col; + Point *rptr, *cptr; + double x0, sigma; + int X0, Y0, X, Y; + #ifndef GSL_FOUND + return FALSE; + #endif + if(!check_box(window, box, &X0, &Y0, &X, &Y)) return FALSE; + if(!crds) return FALSE; + row.data = calloc(box->w, sizeof(Point)); + row.n = box->w; + col.data = calloc(box->h, sizeof(Point)); + col.n = box->h; + cptr = col.data; + for(y = Y0; y < Y; y++, cptr++){ + data = &window->image->data[(HT - y) * WD + X0]; + cptr->x = (double)y; + rptr = row.data; + for(x = X0; x < X; x++, data++, rptr++){ + double d = (double) *data; + rptr->y += d; + cptr->y += d; + } + } + rptr = row.data; + for(x = X0; x < X; x++, rptr++) rptr->x = (double)x; + // we inscribe Gaussian and change the the coordinates and half-widths of the spot + gauss_fit(&row, NULL, NULL, &sigma, &x0); + crds->x = x0; crds->rx = sigma; + gauss_fit(&col, NULL, NULL, &sigma, &x0); + crds->y = x0; crds->ry = sigma; + _FREE(row.data); _FREE(col.data); + return TRUE; +} + +// count of bits amount in word i +// http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel +// http://en.wikipedia.org/wiki/Hamming_weight +int count_bits(l_uint32 i){ + i = i - ((i >> 1) & 0x55555555); + i = (i & 0x33333333) + ((i >> 2) & 0x33333333); + return ((i + ((i >> 4) & 0xF0F0F0F)) * 0x1010101) >> 24; +} + +gboolean get_circle_center(Window *window, BOX *box, PIX *pix, Coordinates *crds){ + int WD = window->image->width, HT = window->image->height - 1; + int N = 0, X0, Y0, X, Y, n; + l_uint32 *pixdata, *pixptr; + l_int32 w, h, d, wpl, sz, i, x, y; + double Xcenter, Ycenter, Radius, xx, yy, Intens, X2, Y2; + GLfloat *data; + if(!check_box(window, box, &X0, &Y0, &X, &Y)) return FALSE; + if(!(pix && crds)) return FALSE; + pixGetDimensions(pix, &w, &h, &d); + if(d != 1) return FALSE; + wpl = pixGetWpl(pix); + pixdata = pixGetData(pix); + pixptr = pixdata; + sz = wpl * h; + for(i = 0; i < sz; i++) N += count_bits(*pixptr++); + if(N < 200000) return FALSE; + n = 0; + Xcenter = (double)(X0 + X) / 2.; + Ycenter = (double)(Y0 + Y) / 2.; + Radius = (double)(w + h) / 4.; + DBG("x0=%g, y0=%g, r0=%g, N=%d", Xcenter, Ycenter, Radius, N); + Xcenter = Ycenter = X2 = Y2 = Intens = 0.; + AxisX = 0.; AxisY = (double)HT; + for(y = 0; y < h; y++){ + yy = (double)(HT - y - Y0); + data = &window->image->data[(HT - y - Y0) * WD + X0]; + l_uint32 *line = pixdata + y * wpl; + for(x = 0; x < w; x++, data++){ + if(GET_DATA_BIT(line, x)){ + double d = *data; + xx= (X0 + x); + n++; + Intens += d; + Xcenter += xx * d; + Ycenter += yy * d; + X2 += xx*xx*d; + Y2 += yy*yy*d; + } + } + } + xx = Xcenter / Intens; + yy = Ycenter / Intens; + crds->x = xx; + crds->y = yy; + crds->rx= sqrt(X2 / Intens - xx*xx); + crds->ry= sqrt(Y2 / Intens - yy*yy); + DBG("xx: %g, yy: %g, rx:%g, ry: %g", xx, yy, crds->rx, crds->ry); + return TRUE; +} + +// Detection of spots' center, yet lousy through gaussians +void get_spots_coords(Window *window, IMAGE *ima){ + BOXA *boxa; + BOX *box; + PIX *pixs; + Spot spot; + Spots *spots; + Coordinates scrds; + int n, i, k; + float xc, yc; + if(!( /*(window->context->transformMask & TRANS_TRES) &&*/ window->image_transformed->data)){ + g_err(_("Define treshold limits first")); + window->context->graphMode = GR_HISTOGRAM; + show_histogram(window); + return; + } + pixs = conv_image2pix(ima); + boxa = pixConnComp(pixs, NULL, 8); + n = boxaGetCount(boxa); + red("Number of boxes: %d", n); + spots_free(&window->spots); + if(!(window->spots = spots_alloc(n))) return; + spots = window->spots; + #ifndef GSL_FOUND + g_err(_("No GSL library found, don't calculate centroids")); + #endif + for(i = 0, k = 0; i < n; i++){ + if((box = boxaGetBox(boxa, i, L_CLONE)) == NULL){ + g_err(_("Box not found")); + continue; + } + if( box->w < Global->minSpotW || + box->h < Global->minSpotH || + box->w > Global->maxSpotW || + box->h > Global->maxSpotH){ + boxDestroy(&box); + continue; + } + if(!get_gaussian_center(window, box, &scrds)){ + scrds.rx = ((double)box->w-1.)/2.; + scrds.ry = ((double)box->h-1.)/2.; + xc = box->x + scrds.rx; + yc = box->y + scrds.ry; + scrds.x = xc; scrds.y = yc; + } + spot.c = scrds; spot.id = k++; + spot.box.x = box->x; + spot.box.y = box->y; + spot.box.w = box->w; + spot.box.h = box->h; + spots_add(spots, &spot, BY_COPY); + boxDestroy(&box); + } + red("Number of spots: %d", k); + boxaDestroy(&boxa); + gdk_window_invalidate_rect(window->drawingArea->window, + &window->drawingArea->allocation, FALSE); + DBG("return"); +} + +void hartmann_spots(Window *window){ // 'r' + Spots *spots = window->spots; + if(!spots){ + g_err(_("Find spots first")); + return; + } + int n = spots->n; + if(n > MAX_SPOT_NO){ + g_err(_("Too many points")); + return; + } + if(n < MIN_SPOT_NO){ + g_err(_("Not enough points")); + return; + } + sort_spots(window); + window->context->transformMask |= TRANS_HARTMANN; // points are sorted + gdk_window_invalidate_rect(window->drawingArea->window, + &window->drawingArea->allocation, FALSE); +} + +void get_circles_params(Window *window, IMAGE *ima){ + BOXA *boxa; + PIXA *pixa; + BOX *box; + PIX *pixs, *pix; + Spot spot; + Coordinates scrds; + Spots *spots; + int n, i; + if(!( (window->context->transformMask & TRANS_TRES) && window->image_transformed->data)){ + g_err(_("Define treshold limits first")); + window->context->graphMode = GR_HISTOGRAM; + show_histogram(window); + return; + } + pixs = conv_image2pix(ima); + + boxa = pixConnComp(pixs, &pixa, 8); + n = boxaGetCount(boxa); + + red("Number of boxes: %d", n); + spots_free(&window->spots); + if(!(window->spots = spots_alloc(n))) return; + spots = window->spots; + for(i = 0; i < n; i++){ + if((box = boxaGetBox(boxa, i, L_CLONE)) == NULL){ + g_err(_("Box not found")); + continue; + } + if((pix = pixaGetPix(pixa, i, L_CLONE)) == NULL){ + g_err(_("Pix not found")); + continue; + } + if(get_circle_center(window, box, pix, &scrds)){ + spot.c = scrds; spot.id = i; + spot.box.x = box->x; + spot.box.y = box->y; + spot.box.w = box->w; + spot.box.h = box->h; + spots_add(spots, &spot, BY_COPY); + } + boxDestroy(&box); + } + + boxaDestroy(&boxa); + pixaDestroy(&pixa); + + gdk_window_invalidate_rect(window->drawingArea->window, + &window->drawingArea->allocation, FALSE); + DBG("return"); +} +#else // LEPTONICA_FOUND not defined +void get_spots_coords(Window *window __attribute__((unused)), + IMAGE *ima __attribute__((unused))){ + LEPTERR; +} +gboolean get_gaussian_center(Window *window __attribute__((unused)), + BOX *box __attribute__((unused)), Coordinates *crds __attribute__((unused))){ + LEPTERR; + return FALSE; +} +PIX *conv_image2pix(IMAGE *ima __attribute__((unused))){ + LEPTERR; + return NULL; +} +void get_circles_params(Window *window __attribute__((unused)), + IMAGE *ima __attribute__((unused))){ + LEPTERR; +} +#endif // LEPTONICA_FOUND + + +/* + * Build the histogram of picture ima + * The default is 256 shades of gray + */ +gboolean make_histogram(IMAGE *ima){ + FNAME(); + ssize_t i, sz; + int j, pt, *hdata, max, min; // hmax + GLfloat *ptr, hsize, datamin = ima->stat.min, datawd = ima->stat.max - datamin; + #ifdef EBUG + double t0 = dtime(); + #endif + if(!ima->data){ + g_err(_("Image is empty")); + return FALSE; + } + _FREE(ima->stat.histogram.data); + // to change the size of the histogram replace HIST_SIZE by a variable value + ima->stat.histogram.data = calloc(HIST_SIZE+1, sizeof(int)); + if(!ima->stat.histogram.data){ + g_err(_("Can't allocate memory for histogram")); + return FALSE; + } + hdata = ima->stat.histogram.data; + ima->stat.histogram.size = HIST_SIZE; + hsize = (GLfloat) HIST_SIZE / datawd; + ima->stat.histogram.scale = 1. / hsize; + //hmax = HIST_SIZE - 1; + ptr = ima->data; + sz = (ssize_t)(ima->width) * (ssize_t)(ima->height); + DBG("datamin=%g, hsize=%g, sz=%zd, scale=%g", datamin, hsize, sz, ima->stat.histogram.scale); + #pragma omp parallel for private(i, pt) shared(hdata) + for(i = 0; i < sz; i++){ + pt = (int)((ptr[i] - datamin) * hsize); + #pragma omp atomic + hdata[pt]++; + } +/* for(i = 0; i < sz; i++, ptr++){ + pt = (int)(*ptr * hsize); + if(pt < 0) pt = 0; + else if(pt > hmax) pt = hmax; + hdata[pt]++; + }*/ + max = min = hdata[0]; + for(j = 1; j < HIST_SIZE; j++){ + pt = hdata[j]; + if(max < pt) max = pt; + else if(min > pt) min = pt; + } + ima->stat.histogram.max = max; + ima->stat.histogram.min = min; + ima->stat.histogram.bot_tres = -1.; + ima->stat.histogram.top_tres = -1.; + DBG("max: %d, min: %d; time: %g", max, min, dtime() - t0); + return TRUE; +} + +/* + * Fill the structure image_transformed by adjusted by the threshold values + * Bottom - the lower bound, top - upper bound (in units of image intensity) + * Brightness less than bottom, replaced by zero + * Brightness larger than top, replaced by a unit + * The rest - are calculated linearly (equalization histogram) + */ +void treshold_image(Window *window, GLfloat bottom, GLfloat top){ + FNAME(); + ssize_t i, sz; + IMAGE *ima = window->image; + IMAGE *i_new = window->image_transformed; + GLfloat *p_old, *p_new, pt, wd; + GLfloat scale = ima->stat.histogram.scale * (GLfloat)ima->stat.histogram.size; + #ifdef EBUG + double t0 = dtime(); + #endif + if(!(p_old = ima->data)){ + g_err(_("Image is empty")); + return; + } + _FREE(i_new->data); + if((top - bottom) < 0.01) top = bottom + 0.01; + sz = (ssize_t)(ima->width) * (ssize_t)(ima->height); + i_new->data = malloc(sz * sizeof(GLfloat)); + i_new->width = ima->width; i_new->height = ima->height; + i_new->bZero = 0.; i_new->bScale = 1.; + i_new->maxVal = ima->maxVal; + i_new->stat.max = 1.;//top * ima->stat.max; + i_new->stat.min = 0.; + p_new = i_new->data; + DBG("top=%g, bottom=%g, scale=%g", top, bottom, ima->stat.histogram.scale); + top = top * scale+ima->stat.min; + bottom = bottom * scale+ima->stat.min; + DBG("top=%g, bottom=%g", top, bottom); + wd = top - bottom; + for(i = 0; i < sz; i++, p_new++, p_old++){ + pt = *p_old; + if(pt < bottom) pt = 0.; + else if(pt > top) pt = 1.; + else pt = (pt - bottom) / wd; + *p_new = pt; + } + DBG("time: %g", dtime() - t0); + window->context->transformMask |= TRANS_TRES; + window->context->current_image = window->image_transformed; + gen_texture(window->image_transformed, window, TRUE); +} + +const int SINCOSSIZE = 540; // size of the array of sines & cosines + // [-90 , +180) degrees + +void hough_lines(Window *window){ // key 'i' + FNAME(); + int w, h, Rmax, sz; + double hd, wd; + IMAGE *ima = window->image; + Window *outwin = init_window(window, OPENGL_WINDOW); + IMAGE *i_new = outwin->image; + if(!ima->data){ + g_err(_("Image is empty")); + return; + } + _FREE(i_new->data); + w = ima->width; h = ima->height; + wd = (double)w; hd = (double)h; + Rmax = (int)(0.5 + sqrt(wd*wd + hd*hd)); + sz = SINCOSSIZE * Rmax; + DBG("sz: %d", sz); + i_new->data = calloc(sz, sizeof(GLfloat)); + if(!i_new->data){ + g_err(_("Can't allocate memory for Hough transform")); + return; + } + i_new->width = Rmax; i_new->height = SINCOSSIZE; + i_new->bZero = 0.; i_new->bScale = 1.; + i_new->maxVal = 1.; + i_new->dtype = FLOAT_IMG; + gchar *str = malloc(256); + snprintf(str, 255, "Hough transform of file %s", ima->filename); + init_keylist(i_new); + add_key(i_new, "TSTRING", "OBJECT", "Hough", str, TSTRING); + _FREE(str); +#ifdef EBUG + double t0 = dtime(); +#endif + if(!fill_hough_lines(ima->data, ima->stat.min, ima->stat.max, w, h, Rmax, SINCOSSIZE, i_new->data)){ + g_err(_("Error in Hough transform module")); + destroy_window(outwin); + return; + } + DBG("Hough transform for lines, time: %gs", dtime() - t0); + //Context->transformMask |= TRANS_TRES; + //DBG("HOUGH: maxR=%d, size=%d, max: %g at R=%d, theta=%g", + //i_new->width, i_new->height, max, R%Rmax, ((double)(R/Rmax))/2.-90.); + outwin->context->current_image = outwin->image; + compute_minmax(outwin->image); + gen_texture(outwin->image, outwin, FALSE); + force_redraw(outwin->drawingArea); + outwin->image->imagetype = HOUGH; + CH_WIN_TITLE(outwin, "%s (Hough) %s", PRGNAME, window->image->filename); +} + +/* + * respect to the coordinates (x, y) of a click in the window of a Hough transform + * we obtain the parameters of the line (R, phi) and display in the parent window + * the corresponding line + */ +void get_houg_line( Window *window, + double x, double y, // mouse click coordinates + double *phi_, double *R_ // Output: parameters of line + ){ + double R, phi, ang_sep, sinphi, cosphi; + double x1=0.,y1=0., x2=0.,y2=0., yl,yr, xu,xd, X1, X2, Y1, Y2; + if(!window || !window->parent) return; + double W = window->parent->image->width-1., H = window->parent->image->height-1.; + int rdy = 0; + IMAGE *image = window->image; + conv_mouse_to_image_coords(x, y, &R, &phi, window); + ang_sep = 270. / (double)image->height / 180. * M_PI; + phi = phi*ang_sep - M_PI/2.; + sinphi = sin(phi); cosphi = cos(phi); + phi *= 180./M_PI; // convert rad => deg + DBG("line: phi = %g, R = %g", phi, R); + *phi_ = phi; *R_ = R; + GdkDrawable *d = window->parent->drawingArea->window; + static GdkGC *gc = NULL; + if(!gc){ + gc = gdk_gc_new(d); + gdk_gc_set_foreground(gc, &(window->parent->drawingArea->style->white)); + gdk_gc_set_function(gc, GDK_XOR); + } + yl = R/sinphi; yr = (R-W*cosphi)/sinphi; // left & right bounds of a picture + xd = R/cosphi; xu = (R-H*sinphi)/cosphi; // down & up bounds + DBG("yl=%g, yr=%g, xd=%g, xu=%g",yl,yr,xd,xu); + if(yl < H && yl > -0.5){ + DBG("left selected"); + y1 = yl; x1 = 0.; rdy = 1; + } + if(yr < H && yr > -0.5){ + DBG("right selected"); + if(rdy == 0){ + y1 = yr; x1 = W; rdy = 1; + }else{ + y2 = yr; x2 = W; rdy = 2; goto lines_selected; + } + } + if(xd < W && xd > -0.5){ + DBG("down selected"); + if(rdy == 0){ + x1 = xd; y1 = 0.; rdy = 1; + }else{ + x2 = xd; y2 = 0.; rdy = 2; goto lines_selected; + } + } + if(xu < W && xu > -0.5){ + DBG("up selected"); + if(rdy == 0){ + x1 = xu; y1 = H; rdy = 1; + }else{ + x2 = xu; y2 = H; rdy = 2; + } + } +lines_selected: + if(rdy != 2) DBG("WTF? not two points"); + else{ + DBG("line in image CS: (%g, %g) - (%g, %g)", x1,y1,x2,y2); + conv_image_to_mouse_coords(x1, y1, &X1, &Y1, window->parent); + conv_image_to_mouse_coords(x2, y2, &X2, &Y2, window->parent); + gdk_draw_line(d, gc, X1,Y1, X2,Y2); + DBG("line in window CS: (%g, %g) - (%g, %g)", X1,Y1,X2,Y2); + } +} + +// Filtering of an picture window->image by filter f +void filter_image( Window *window, + Filter *f + ){ + float *src, *dst; + int w,h; + gboolean res = FALSE; + // If picture already transformed + if(window->context->current_image == window->image_transformed){ + src = window->image_transformed->data; + w = window->image_transformed->width; + h = window->image_transformed->height; + }else{ + src = window->image->data; + w = window->image->width; + h = window->image->height; + if(window->image_transformed) _FREE(window->image_transformed->data); +// window->image_transformed = COPY(window->image, IMAGE); +// window->image_transformed->data = NULL; + } + switch(f->FilterType){ + case MEDIAN: + res = MedFilter(src, &dst, f, w, h); + break; + case SIMPLEGRAD: + res = GradFilterSimple(src, &dst, f, w, h); + break; + case STEP: // "posterisation" + res = StepFilter(src, &dst, f, w, h, window->image->stat.min, window->image->stat.max, NULL); + break; + default: // differential filters + res = DiffFilter(src, &dst, f, w, h); + } + if(!res){ g_err(_("Error in image filter module")); return; } + window->context->transformMask |= TRANS_FILTER; + _FREE(src); + window->image->data = dst; + window->context->current_image = window->image; + compute_minmax(window->image); + //gen_texture(window->image_transformed, window, TRUE); + gen_texture(window->image, window, TRUE); + force_redraw(window->drawingArea); +} + diff --git a/src/include/CUtools.h b/src/include/CUtools.h new file mode 100644 index 0000000..f894f1e --- /dev/null +++ b/src/include/CUtools.h @@ -0,0 +1,51 @@ +// CUtools.h - common definitions and functions for CUDA.c and NOCUDA.c +#ifndef _CUTOOLS_H_ +#define _CUTOOLS_H_ +#ifdef _CUDA_CU_ // file was included from CUDA.cu + #define EXTERN extern "C" +#else + #define EXTERN +#endif +#ifndef _GNU_SOURCE + #define _GNU_SOURCE +#endif +#include +#include +#include + +typedef struct{ + unsigned char FilterType; // filter type + int w; // filter width + int h; // height + double sx; // x half-width + double sy; // y half-width (sx, sy - for Gaussian-type filters) +} Filter; +// FilterType +enum{ + LAPGAUSS // laplasian of gaussian + ,GAUSS // gaussian + ,SOBELH // Sobel horizontal + ,SOBELV // -//- vertical + ,SIMPLEGRAD // simple gradient (by Sobel) + ,PREWITTH // Prewitt (horizontal) - simple derivative + ,PREWITTV // -//- (vertical) + ,MEDIAN // median + ,STEP // "posterisation" +}; +// f->h for STEP filter +enum{ + UNIFORM + ,LOG + ,EXP + ,SQRT + ,POW +}; + +// export section +EXTERN int fill_hough_lines(float *ima, float min, float max, int imW, int imH, int Rmax, int angles, float *hough); +EXTERN int DiffFilter(float *ima, float **result, Filter *f, int sizex, int sizey); +EXTERN int MedFilter(float *ima, float **result, Filter *f, int sizex, int sizey); +EXTERN int fillIsoScale(Filter *f, float **scale, float min, float wd); +EXTERN int StepFilter(float *ima, float **result, Filter *f, int sizex, int sizey, float min, float max, float **scale); +EXTERN int GradFilterSimple(float *ima, float **result, Filter *f, int sizex, int sizey); +#endif // _CUTOOLS_H_ diff --git a/src/include/contours.h b/src/include/contours.h new file mode 100644 index 0000000..2543b21 --- /dev/null +++ b/src/include/contours.h @@ -0,0 +1,9 @@ +#ifndef __CONTOURS_H__ +#define __CONTOURS_H__ +#include "fitsview.h" +#include "gtk.h" +#include "spots.h" +void find_contours(Window *window, int n, int type); +void free_contours(IMAGE *image); +int copy_contours(IMAGE *in, IMAGE *out, float dX, float dY); +#endif // __CONTOURS_H__ diff --git a/src/include/filelist.h b/src/include/filelist.h new file mode 100644 index 0000000..e985f4d --- /dev/null +++ b/src/include/filelist.h @@ -0,0 +1,19 @@ +#ifndef __FILELIST_H__ +#define __FILELIST_H__ + +#include "fitsview.h" +#include "gtk.h" + +enum{ + CURRENT, + FIRST, + LAST, + PREVIOUS, + NEXT +}; + +void free_filelist(Window *window); +gint fill_filelist(gchar *filename, Window *window); +gchar *get_filename(guchar type, Window *window); +gboolean get_ext(gchar *filename); +#endif // __FILELIST_H__ diff --git a/src/include/fits.h b/src/include/fits.h new file mode 100644 index 0000000..106f9f1 --- /dev/null +++ b/src/include/fits.h @@ -0,0 +1,11 @@ +#ifndef _FITS_H_ +#define _FITS_H_ +#include "fitsview.h" + +#define SCALE_MIN (0.01) // scale smaller than this is wrong +gboolean cmplx_conv(gchar *value, double *re, double *im); +gboolean readfits(gchar *filename, IMAGE *data); +gboolean writefits(gchar *filename, IMAGE *data); +gboolean try_open_file(gchar *filename, IMAGE *data); +gboolean guess_fits(gchar *filename); +#endif // _FITS_H_ diff --git a/src/include/fitsheaders.h b/src/include/fitsheaders.h new file mode 100644 index 0000000..8871a33 --- /dev/null +++ b/src/include/fitsheaders.h @@ -0,0 +1,20 @@ +#ifndef _FITSHEADERS_H_ +#define _FITSHEADERS_H_ +#include "fitsview.h" +#include "gtk.h" + +enum{ // key parameters field + L_TYPENM, // type name + L_KEY, // key name + L_VAL, // key value + L_COMM, // comment + L_TYPE, // key type (invisible column, we need you?) + L_MAX // amount of parameters +}; + +void edit_headers(Window *parent); +void add_key( IMAGE *ima, + gchar *keytype, gchar *keyname, gchar *keyval, + gchar *keycomment, int keyfitstype); +void init_keylist(IMAGE *ima); +#endif diff --git a/src/include/fitsview.h b/src/include/fitsview.h new file mode 100644 index 0000000..066de17 --- /dev/null +++ b/src/include/fitsview.h @@ -0,0 +1,233 @@ +#ifndef _FITSVIEW_H_ +#define _FITSVIEW_H_ + +#ifndef _GNU_SOURCE + #define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // for locale +#include // for dtime +#include // for basename +#include +#include +#include +#include +#include // for color text in tty + + +#include "CUtools.h" + +#ifndef GETTEXT_PACKAGE +#define GETTEXT_PACKAGE "fitsview" +#endif +#define PRGNAME GETTEXT_PACKAGE +#ifndef LOCALEDIR +#define LOCALEDIR "/home/eddy/locale" +#endif + +#define _(String) gettext(String) +#define gettext_noop(String) String +#define N_(String) gettext_noop(String) + +#ifndef THREAD_NUMBER + #define THREAD_NUMBER 2 +#endif +#ifndef OMP_NUM_THREADS + #define OMP_NUM_THREADS THREAD_NUMBER +#endif + +extern const int HIST_SIZE; +void *copy_struct(void *src, int size); +void *init_struct(int size); +#define INIT(var, type) var = (type*)init_struct(sizeof(type)) +#define COPY(src, type) (type*)copy_struct((void*)src, sizeof(type)) +#define _FREE(ptr) do{free(ptr); ptr = NULL;}while(0) + +/* + * If the distance from the current position of the mouse to a certain point + * less than this value, it is believed that this kind of chosen point + */ +#define PT_TRESHOLD 5 + +// debug mode, -DEBUG +#ifdef EBUG + #define FNAME() fprintf(stderr, "\n%s (%s, line %d)\n", __func__, __FILE__, __LINE__) + #define DBG(...) do{fprintf(stderr, "%s (%s, line %d): ", __func__, __FILE__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, "\n");} while(0) +#else + #define FNAME() do{}while(0) + #define DBG(...) do{}while(0) +#endif //EBUG + +extern int (*red)(const char *fmt, ...); +extern int (*green)(const char *fmt, ...); + +// contour poing +typedef struct _cPoint{ + float x; // coordinates of the contour point in the CS picture + float y; + struct _cPoint *prev;// pointers to the previous and next points of the contour + struct _cPoint *next; +} cPoint; +// contour structure +typedef struct _Contour{ + int N; // amount of points + cPoint *first; // the first point of the contour (list) + cPoint *last; // last -//- +// struct _Contour *prev; // pointers to the previous and following contour of the group + struct _Contour *next; + unsigned char closed;// == TRUE, if contour is closed +} Contour; +typedef struct{ + int L; // level (at the lower boundary of 0, 1, etc) + int N; // number of units in the structure + Contour *first; // pointer to the first contour in the list + Contour *last; // pointer to the last contour in the list +}cList; + +// image histogram +typedef struct{ + int size; // it is possible that the size of the histogram can be changed + int max; // maximum and minimum values of the histogram (the amplitude) + int min; + double bot_tres; // upper and lower thresholds for equalization (normalized to 1) + double top_tres; + GLfloat scale; // the scale - the range of intensities per unit of histogram + int *data; +} ImageHistogram; +// image statistics +struct ImageStat{ + GLfloat max; + GLfloat min; + ImageHistogram histogram; + /*GLfloat median; + GLfloat mean; + GLfloat std;*/ +}; + +// image itself +typedef struct{ + int width; // width + int height; // height + float bZero; // conditional zero point + float bScale; // scale + float maxVal; // the limiting value of the intensity (1<image or window->image_transformed) +} Context; + +typedef struct{ + unsigned char previewMode; // preview settings + gboolean add_all; // try to add all the files (or only .fts / .fit [s]) + int minSpotW; // the limit sizes of points + int maxSpotW; + int minSpotH; + int maxSpotH; +} Global_context; + +extern Global_context *Global; + +/* + * description fields of the structure of context + * the first (zero) should go the default values + */ +// drawingMode: +enum{ + DRAW_TRACK // tracking mode + ,DRAW_SELECTION // and so on + ,DRAW_QUAD + ,GAME_MODE +}; +// trackingMode: +enum{ + TRACK_ANY + ,TRACK_VERT + ,TRACK_HORIZ + ,TRACK_DIAG +}; +// graphYaxis, may be masking: LSB - the graph, Sr. - histogram +enum{ + Y_LINEAR = 0 + ,Y_LOGBOTH = 3 + ,Y_LOGHIST = 2 + ,Y_LOGGRAPH= 1 +}; +// graphMode - what is shown on graph (histogram or track) +enum{ + GR_GRAPH + ,GR_HISTOGRAM +}; +// transformMask - bit components +enum{ + TRANS_NONE = 0 + ,TRANS_TRES = 1 + ,TRANS_ROT = 2 + ,TRANS_MOVE = 4 + ,TRANS_SCALE = 8 + ,TRANS_FILTER = 16 + ,TRANS_HARTMANN = 32 +}; +// visualMode - bit components +enum{ + SHOW_ONLY_IMAGE = 0 + ,SHOW_POTBOXES = 1 + ,SHOW_POTSELECT = 2 + ,SHOW_ISOLINES = 4 + ,SHOW_ALL = 255 +}; +// previewMode - bit components, masks +enum{ + PREVIEW_DEFAULT // (512x512, sqrt) + ,PREVIEW_128 + ,PREVIEW_256 + ,PREVIEW_512 + ,PREVIEW_768 + ,PREVIEW_1024 + ,PREVIEW_SIZEMASK = 7 + ,PREVIEW_LINEAR = 8 + ,PREVIEW_LOG = 16 + ,PREVIEW_SQRT = 24 + ,PREVIEW_COLFNMASK = 24 +}; + +double dtime(); + +#endif // _FITSVIEW_H_ diff --git a/src/include/gauss.h b/src/include/gauss.h new file mode 100644 index 0000000..5e71924 --- /dev/null +++ b/src/include/gauss.h @@ -0,0 +1,20 @@ +#ifndef _GAUSS_H_ +#define _GAUSS_H_ +#include "fitsview.h" +#include "tracking.h" +#ifndef GSL_FOUND + #define GSLERR g_err(_("Install GSL library and remake sources for this functions")) +#endif +struct data { + size_t n; + double *x; + double *y; + double *dy; +}; + +double gaussian_pt(double C, double A, double sigma, double x0, double x); +void gaussian_v(double C, double A, double sigma, double x0, Points *pts); +void gauss_fit(Points *pts, double *C_, double *A_, double *sigma_, double *x0_); +/*void circle_fit(struct data *data, double *Xcenter, + double *Ycenter, double *Radius);*/ +#endif // _GAUSS_H_ diff --git a/src/include/gtk.h b/src/include/gtk.h new file mode 100644 index 0000000..40b162d --- /dev/null +++ b/src/include/gtk.h @@ -0,0 +1,92 @@ +#ifndef _GTK_H_ +#define _GTK_H_ +#include "fitsview.h" +#define EVENT_METHOD(i, x) GTK_WIDGET_GET_CLASS(i)->x + +#define CH_WIN_TITLE(wndw, ...) do{ \ + gchar *title = g_strdup_printf(__VA_ARGS__); \ + gtk_window_set_title(GTK_WINDOW(wndw->window), title);\ + g_free(title); \ + }while(0) + +// Maximum number of cells lines the status window +#define SBAR_MAX 4 +// Status bar fields +enum{ + StatusText + ,StatusState + ,StatusCoords + ,StatusAdd +}; + +typedef struct{ + GLuint tex; // texture itself + GLuint w; // its size + GLuint h; +} TEXTURE; + +typedef struct{ + double x; + double y; +} XY; + +// List of files in the current directory for this window +typedef struct{ + gint list_length; + GList *files_list; + GList *list_current; + GList *list_end; +} FITSlist; + +struct Spots; +// window description structure +struct Window{ + int id; // identificator: MAIN_WINDOW, GRAPH_WINDOW, OPENGL_WINDOW + GtkWidget *window; // pointer to a window + Context *context; // context of a window + struct Window *parent; // parent window (NULL for the main) + struct Window *graphWindow; // window's graph (while there's no - NULL) + GtkWidget *drawingArea; // drawing area (openGL) + GtkWidget *hRule; // rulers + GtkWidget *vRule; + GtkEntry *SBars[SBAR_MAX]; // an array of pointers to the cells of the status bar + guint statusBlocks; // status bar blocks amount + IMAGE *image; // image for a window + IMAGE *image_transformed; // and its transformation + struct Spots *spots; // spots array for a window + TEXTURE *texture; // texture or VBO buffers + GtkRadioAction *LinLogMenu[2]; // scale lin/log + double Zoom; // zoom scale + double Daspect; // scale in image region (in window) + XY move; // image motions (in window) + XY mouse; // .x, .y - the beginning of the window coordinates in SC of a picture + GLfloat Xangle; // angles rotation relative to the X and Z axes + GLfloat Zangle; + FITSlist files; // files in current directory for this window +}; +typedef struct Window Window; +// window id +enum{ + MAIN_WINDOW, + GRAPH_WINDOW, + OPENGL_WINDOW, + GL3D_WINDOW +}; + +extern Window *mainWindow; + +void change_image(gchar *filename, Window *window); +void init_main_window(int *argc, char ***argv); +Window *init_window(Window *parent, int winId); +void destroy_window(Window* window); +void run_modal_window(GtkWindow *w, Window *parent); +gint run_modal_dialog(GtkDialog *dialog, Window *parent); +void g_err(gchar *text); +void set_Drulers(double x0, double y0, double xm, double ym, Window *window); +void set_Grulers(double y0, double xm, double ym, Window *window); +void show_histogram(Window *window); +void set_status_text(guint barName, gchar *text, Window *window); +void refresh_state(Window *window); +gchar *get_open_filename(Window *window); +void get_prefocal(Window *window, gboolean *prefocal); +#endif // _GTK_H_ diff --git a/src/include/imtools.h b/src/include/imtools.h new file mode 100644 index 0000000..98c5d73 --- /dev/null +++ b/src/include/imtools.h @@ -0,0 +1,19 @@ +#ifndef _IMTOOLS_H_ +#define _IMTOOLS_H_ +#include "fitsview.h" +#include "gtk.h" +#include "spots.h" + +void destroy_image(IMAGE *image); +void free_contours(IMAGE *image); +gboolean make_histogram(IMAGE *ima); +void treshold_image(Window *window, GLfloat bottom, GLfloat top); +void get_spots_coords(Window *window, IMAGE *ima); +void hartmann_spots(Window *window); +gboolean get_gaussian_center(Window *window, BOX *box, Coordinates *crds); +PIX *conv_image2pix(IMAGE *ima); +void get_circles_params(Window *window, IMAGE *ima); +void hough_lines(Window *window); +void get_houg_line(Window *window, double x, double y, double *phi_, double *R_); +void filter_image(Window *window, Filter *f); +#endif // _IMTOOLS_H_ diff --git a/src/include/open_dialog.h b/src/include/open_dialog.h new file mode 100644 index 0000000..b3a1893 --- /dev/null +++ b/src/include/open_dialog.h @@ -0,0 +1,8 @@ +#ifndef _OPEN_DIALOG_H_ +#define _OPEN_DIALOG_H_ +#include "gtk.h" + +extern gchar *saved_path; +GtkFileChooser *open_fits_dialog(); +void gray2rgb(float gray, guchar *rgb); +#endif diff --git a/src/include/opengl.h b/src/include/opengl.h new file mode 100644 index 0000000..6a9b814 --- /dev/null +++ b/src/include/opengl.h @@ -0,0 +1,21 @@ +#ifndef _OPENGL_H_ +#define _OPENGL_H_ +#define GL_GLEXT_PROTOTYPES +#include "fitsview.h" +#include +#include +#include +#include "gtk.h" + +extern gboolean useVBO; + +void initGl(GtkWidget *drawingArea); +void gen_texture(IMAGE *image, Window *window, gboolean redraw); +void force_redraw(GtkWidget *Area); +void pickRegion(double x, double y); +void conv_mouse_to_image_coords(double x, double y, double *X, double *Y, Window *window); +void conv_image_to_mouse_coords(double x, double y, double *X, double *Y, Window *window); +void init3D(Window *window); +void getGLinfo(GtkWidget *Area); +void freeGLmemory(Window *window); +#endif // _OPENGL_H_ diff --git a/src/include/spots.h b/src/include/spots.h new file mode 100644 index 0000000..faad810 --- /dev/null +++ b/src/include/spots.h @@ -0,0 +1,61 @@ +#ifndef _SPOTS_H_ +#define _SPOTS_H_ +#include "fitsview.h" + +#ifndef LEPTONICA_FOUND + #define LEPTERR g_err(_("Install Leptonica library and remake sources for this functions")) + typedef struct{double x; double y; double w; double h;} BOX; + typedef void* PIX; +#else + #include // from leptonica +#endif // LEPTONICA_FOUND + +/* + * Coordinates of the center of area and its sizes was originally initiated + * based on the box-structures, then adjusted to a coordinate system relative + * to the optical axis, further refined by a Gaussian + * (x and y - as the central parameters of Gaussians, + * rx and ry - as the standard deviation of a Gaussian + */ +typedef struct{ + double x; + double y; // center coordinates + double rx; + double ry; // half-sizes +} Coordinates; + +// spot parameters +typedef struct{ + BOX box; // bounding box + Coordinates c; // dimensions and coordinates in CS relative to the optical axis, without rotation + double r; // module of the radius vector in the CS of the center of spots + double phi; // angular coordinate (from OX counter-clockwise) + double xC; // coordinates of spots in the CS of an optical axis, taking into account the rotation + double yC; + int id; // number, id etc. +} Spot; + +// spots array +struct Spots{ + int size; // array size + int n; // amount of spots in array + int memflag;// BY_COPY or BY_PTR - method of spots allocation + Spot **spot;// spots themselves +}; + +typedef struct Spots Spots; + +Spots* spots_alloc(int n); +Spot* spots_add(Spots *spots, Spot *spot, int copy); +enum { // definition for the function spots_add + BY_COPY, // copy spot + BY_PTR // only insert pointer to a spot +}; +void spots_free(Spots **spots); +struct Window; +void sort_spots(struct Window *window); +void spots_save(Spots *spots, gchar *filename); + +extern double AxisX, AxisY; + +#endif // _SPOTS_H_ diff --git a/src/include/terrain.h b/src/include/terrain.h new file mode 100644 index 0000000..ab2453a --- /dev/null +++ b/src/include/terrain.h @@ -0,0 +1,9 @@ +#ifndef __TERRAIN_H__ +#define __TERRAIN_H__ + +#include "fitsview.h" +#include "gtk.h" + +void terrain_3D(Window *window, double x0, double y0, double x, double y, gboolean from_file); + +#endif // __TERRAIN_H__ diff --git a/src/include/tracking.h b/src/include/tracking.h new file mode 100644 index 0000000..ff2a2ee --- /dev/null +++ b/src/include/tracking.h @@ -0,0 +1,31 @@ +#ifndef _TRACKING_H_ +#define _TRACKING_H_ + +#include "fitsview.h" +#include "gtk.h" + +typedef struct{ + double x; + double y; +} Point; + +typedef struct{ + int n; + Point *data; +} Points; + +extern double gYmax, gXmax; +extern double xScale, yScale; + +void do_tracking(double x, double y, gboolean start, Window *window); +void do_histogram(Window *window); +gboolean graph_mouse_btn(int x, Window *window); +gboolean isYlog(Window *window); + +void gen_tres_texture(Window *window); +gboolean Gexpose(Window *window); +gboolean Gconfigure(Window *window); +gboolean fit_gaussian(Window *window); +void get_image_crds(double x, double *xx, double *yy); +void get_sel_region(double *x1, double *y1, double *x2, double *y2); +#endif // _TRACKING_H_ diff --git a/src/locale/ru/LC_MESSAGES/fitsview.mo b/src/locale/ru/LC_MESSAGES/fitsview.mo new file mode 100644 index 0000000000000000000000000000000000000000..2c92fc9cc44d85bfe2a9373583b418494e397482 GIT binary patch literal 8870 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, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-12-10 17:22+0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: gtk.c:58 +#, c-format +msgid "Error: %s\n" +msgstr "" + +#: gtk.c:174 fits.c:191 +msgid "Can't read fits file" +msgstr "" + +#: gtk.c:185 +msgid "Can't save file" +msgstr "" + +#: gtk.c:367 +msgid "Selected line" +msgstr "" + +#: gtk.c:473 +msgid "Both max values are less than min" +msgstr "" + +#: gtk.c:476 +msgid "Max height value less than min" +msgstr "" + +#: gtk.c:478 +msgid "Max width value less than min" +msgstr "" + +#: gtk.c:525 +msgid "No recognized spots" +msgstr "" + +#: gtk.c:1090 +msgid "Quit" +msgstr "" + +#: gtk.c:1093 +msgid "Close" +msgstr "" + +#: fitsheaders.c:53 +msgid "Type" +msgstr "" + +#: fitsheaders.c:53 +msgid "Name" +msgstr "" + +#: fitsheaders.c:53 +msgid "Value" +msgstr "" + +#: fitsheaders.c:53 +msgid "Comment" +msgstr "" + +#: fitsheaders.c:101 +msgid "Error in value range!" +msgstr "" + +#: fitsheaders.c:120 +msgid "Invalid double number!" +msgstr "" + +#: fitsheaders.c:132 +msgid "Format error: complex number must be in format" +msgstr "" + +#: fitsheaders.c:486 +msgid "Open fits file first" +msgstr "" + +#: fitsheaders.c:513 +msgid "New entry (ctrl+n)" +msgstr "" + +#: fitsheaders.c:518 +msgid "Delete entry (ctrl+d)" +msgstr "" + +#: fitsheaders.c:524 +msgid "Close (ctrl+w)" +msgstr "" + +#: tracking.c:57 tracking.c:62 tracking.c:350 +msgid "Can't allocate memory for track points" +msgstr "" + +#: tracking.c:483 +#, c-format +msgid "Selected region from %.1f to %.1f, %d datapoints" +msgstr "" + +#: tracking.c:518 +msgid "No parent window or image" +msgstr "" + +#: tracking.c:526 +msgid "No data points" +msgstr "" + +#: tracking.c:531 +msgid "Bad amount of data points" +msgstr "" + +#: tracking.c:536 +msgid "Invalid number of first point" +msgstr "" + +#: tracking.c:543 contours.c:394 contours.c:400 contours.c:449 contours.c:453 +#: contours.c:456 terrain.c:264 terrain.c:270 +msgid "Can't allocate memory" +msgstr "" + +#: tracking.c:559 +#, c-format +msgid "Fit gaussian, x0=%.2f I(%.2f, %.2f), s=%.2f, A=%.1f, C=%.3f" +msgstr "" + +#: spots.c:63 +msgid "Too many markers" +msgstr "" + +#: spots.c:64 +msgid "Not enough markers" +msgstr "" + +#: spots.c:65 +msgid "Bad markers angle" +msgstr "" + +#: spots.c:66 +msgid "Too many rings" +msgstr "" + +#: spots.c:83 +msgid "Can't allocate memory for new spots" +msgstr "" + +#: spots.c:88 +msgid "Can't allocate memory for pointers in spots array" +msgstr "" + +#: spots.c:122 +msgid "Spots array is full, can't place new spot\n" +msgstr "" + +#: spots.c:126 +msgid "Zero pointer to spot\n" +msgstr "" + +#: spots.c:132 +msgid "Can't allocate memory for new spot" +msgstr "" + +#: spots.c:137 +msgid "Can't copy spot to new one" +msgstr "" + +#: spots.c:499 +msgid "Can't open file" +msgstr "" + +#: spots.c:511 +msgid "Can't write to file" +msgstr "" + +#: imtools.c:222 imtools.c:302 +msgid "Define treshold limits first" +msgstr "" + +#: imtools.c:235 +msgid "No GSL library found, don't calculate centroids" +msgstr "" + +#: imtools.c:239 imtools.c:318 +msgid "Box not found" +msgstr "" + +#: imtools.c:274 +msgid "Find spots first" +msgstr "" + +#: imtools.c:279 +msgid "Too many points" +msgstr "" + +#: imtools.c:283 +msgid "Not enough points" +msgstr "" + +#: imtools.c:322 +msgid "Pix not found" +msgstr "" + +#: imtools.c:377 imtools.c:439 imtools.c:481 +msgid "Image is empty" +msgstr "" + +#: imtools.c:384 +msgid "Can't allocate memory for histogram" +msgstr "" + +#: imtools.c:492 +msgid "Can't allocate memory for Hough transform" +msgstr "" + +#: imtools.c:508 +msgid "Error in Hough transform module" +msgstr "" + +#: imtools.c:628 +msgid "Error in image filter module" +msgstr "" + +#: NOCUDA.c:809 +msgid "No memory left" +msgstr "" + +#: fits.c:284 +msgid "Can't read HDU" +msgstr "" + +#: fits.c:345 +msgid "Not an image? (dimensions != 2)" +msgstr "" + +#: opengl.c:34 +msgid "OpenGL not supported" +msgstr "" + +#: opengl.c:55 +msgid "" +"\n" +"OpenGL info:\n" +msgstr "" + +#: opengl.c:60 +msgid "Color bits" +msgstr "" + +#: opengl.c:65 +msgid "Depth bits" +msgstr "" + +#: opengl.c:66 +msgid "Stencil bits" +msgstr "" + +#: opengl.c:66 +msgid "Max amount of lights" +msgstr "" + +#: opengl.c:69 +msgid "Max texture size" +msgstr "" + +#: opengl.c:70 +msgid "Max clip planes" +msgstr "" + +#: opengl.c:75 +msgid "Max stack depths" +msgstr "" + +#: opengl.c:76 +msgid "modelview" +msgstr "" + +#: opengl.c:76 +msgid "projection" +msgstr "" + +#: opengl.c:76 +msgid "attrib" +msgstr "" + +#: opengl.c:77 +msgid "texture" +msgstr "" + +#: opengl.c:80 +msgid "VBO is supported\n" +msgstr "" + +#: opengl.c:288 +msgid "Empty region" +msgstr "" + +#: opengl.c:295 +msgid "Selected spot[s]:\n" +msgstr "" + +#: opengl.c:297 +msgid "Selected spot: " +msgstr "" + +#: opengl.c:299 +msgid "Selected spots: " +msgstr "" + +#: opengl.c:483 terrain.c:321 +msgid "Can't allocate memory for texture" +msgstr "" + +#: terrain.c:227 +msgid "Selected area too small" +msgstr "" + +#: terrain.c:233 +msgid "Error occured when tried to add contours" +msgstr "" + +#: terrain.c:256 +msgid "No image in parent window" +msgstr "" + +#: terrain.c:332 +msgid "Can't generate VBO / GL list" +msgstr "" + +#: open_dialog.c:79 +msgid "linear" +msgstr "" + +#: open_dialog.c:79 +msgid "log" +msgstr "" + +#: open_dialog.c:79 +msgid "square root" +msgstr "" + +#: open_dialog.c:96 open_dialog.c:354 +msgid "Preview size" +msgstr "" + +#: open_dialog.c:105 +msgid "Colormap function" +msgstr "" + +#: open_dialog.c:142 +msgid "Select fits file to open" +msgstr "" + +#: open_dialog.c:155 +msgid "FITS files" +msgstr "" + +#: open_dialog.c:164 +msgid "All files" +msgstr "" + +#: open_dialog.c:188 +msgid "Preview settings" +msgstr "" + +#: open_dialog.c:353 +msgid "Image size" +msgstr "" + +#: open_dialog.c:355 +msgid "Preview scale" +msgstr "" + +#: fitsview.glade:16 fitsview.glade:448 fitsview.glade:679 +msgid "_File" +msgstr "" + +#: fitsview.glade:40 +msgid "Open in _new window" +msgstr "" + +#: fitsview.glade:55 +msgid "Open in _3D window" +msgstr "" + +#: fitsview.glade:92 fitsview.glade:474 fitsview.glade:713 +msgid "_Edit" +msgstr "" + +#: fitsview.glade:99 fitsview.glade:481 fitsview.glade:720 +msgid "_View" +msgstr "" + +#: fitsview.glade:107 +msgid "Show _histogram" +msgstr "" + +#: fitsview.glade:115 +msgid "Show _headers" +msgstr "" + +#: fitsview.glade:128 +msgid "3D view of full image" +msgstr "" + +#: fitsview.glade:136 +msgid "3D view of subframe" +msgstr "" + +#: fitsview.glade:144 +msgid "Select _spots" +msgstr "" + +#: fitsview.glade:152 +msgid "Draw _tracks" +msgstr "" + +#: fitsview.glade:165 +msgid "_Zoom frame" +msgstr "" + +#: fitsview.glade:173 fitsview.glade:834 +msgid "_Restore image" +msgstr "" + +#: fitsview.glade:181 +msgid "Zoom _in" +msgstr "" + +#: fitsview.glade:189 +msgid "Zoom _out" +msgstr "" + +#: fitsview.glade:201 fitsview.glade:525 fitsview.glade:846 +msgid "_Math" +msgstr "" + +#: fitsview.glade:209 +msgid "Find and enumerate spots" +msgstr "" + +#: fitsview.glade:210 +msgid "_Spots" +msgstr "" + +#: fitsview.glade:218 +msgid "Choose minimal & maximal spot size" +msgstr "" + +#: fitsview.glade:219 +msgid "Size _tresholds" +msgstr "" + +#: fitsview.glade:227 +msgid "Identify _spots" +msgstr "" + +#: fitsview.glade:235 +msgid "So_rt hartmann spots" +msgstr "" + +#: fitsview.glade:243 +msgid "Sa_ve spots" +msgstr "" + +#: fitsview.glade:255 +msgid "Identify _circles" +msgstr "" + +#: fitsview.glade:263 +msgid "_Hough transform" +msgstr "" + +#: fitsview.glade:271 +msgid "_Filter" +msgstr "" + +#: fitsview.glade:283 +msgid "_Help" +msgstr "" + +#: fitsview.glade:489 +msgid "Y axis scale" +msgstr "" + +#: fitsview.glade:497 +msgid "Linear" +msgstr "" + +#: fitsview.glade:507 +msgid "Log" +msgstr "" + +#: fitsview.glade:533 +msgid "Approximate by _gaussian" +msgstr "" + +#: fitsview.glade:728 +msgid "_Move" +msgstr "" + +#: fitsview.glade:736 +msgid "Rotate X CW" +msgstr "" + +#: fitsview.glade:744 +msgid "Rotate X CCW" +msgstr "" + +#: fitsview.glade:752 +msgid "Rotate Z CCW" +msgstr "" + +#: fitsview.glade:760 +msgid "Rotate Z CW" +msgstr "" + +#: fitsview.glade:768 +msgid "Move right" +msgstr "" + +#: fitsview.glade:776 +msgid "Move left" +msgstr "" + +#: fitsview.glade:784 +msgid "Move down" +msgstr "" + +#: fitsview.glade:792 +msgid "Move up" +msgstr "" + +#: fitsview.glade:800 +msgid "Move backward" +msgstr "" + +#: fitsview.glade:808 +msgid "Move forward" +msgstr "" + +#: fitsview.glade:825 +msgid "Mouse and arrow keys navigation" +msgstr "" + +#: fitsview.glade:826 +msgid "_Game mode" +msgstr "" + +#: fitsview.glade:942 +msgid "Spots' size treshold" +msgstr "" + +#: fitsview.glade:986 fitsview.glade:1084 +msgid "Min" +msgstr "" + +#: fitsview.glade:1018 fitsview.glade:1116 +msgid "Max" +msgstr "" + +#: fitsview.glade:1050 +msgid "Width" +msgstr "" + +#: fitsview.glade:1148 +msgid "Height" +msgstr "" + +#: fitsview.glade:1248 +msgid "Focus value, mm" +msgstr "" + +#: fitsview.glade:1277 +msgid "Prefocal" +msgstr "" + +#: fitsview.glade:1290 +msgid "Postfocal" +msgstr "" + +#: fitsview.glade:1308 +msgid "Image type" +msgstr "" diff --git a/src/locale/ru/ru.po b/src/locale/ru/ru.po new file mode 100644 index 0000000..54bbbec --- /dev/null +++ b/src/locale/ru/ru.po @@ -0,0 +1,590 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "Project-Id-Version: PACKAGE VERSION\n" + "Report-Msgid-Bugs-To: \n" + "POT-Creation-Date: 2011-07-15 11:01+0400\n" + "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" + "Last-Translator: FULL NAME \n" + "Language-Team: LANGUAGE \n" + "MIME-Version: 1.0\n" + "Content-Type: text/plain; charset=koi8-r\n" + "Content-Transfer-Encoding: 8bit\n" + +#: opengl.c:53 +#, fuzzy +msgid "\n" + "OpenGL info:\n" +msgstr "Информация об OpenGL:\n" + +#: fitsview.glade:128 +msgid "3D view of full image" +msgstr "Просмотр всего изображения в 3D" + +#: fitsview.glade:136 +msgid "3D view of subframe" +msgstr "Просмотр части изображения в 3D" + +#: fitsview.glade:1240 +msgid "Focus value, mm" +msgstr "Фокус, мм" + +#: fitsview.glade:1140 +msgid "Height" +msgstr "Высота" + +#: fitsview.glade:1300 +msgid "Image type" +msgstr "Тип изображения" + +#: fitsview.glade:1042 +msgid "Width" +msgstr "Ширина" + +#: opengl.c:76 +msgid "All extensions" +msgstr "Все расширения" + +#: open_dialog.c:164 +msgid "All files" +msgstr "Все файлы" + +#: fitsview.glade:525 +msgid "Approximate by _gaussian" +msgstr "Аппроксимация _гауссианой" + +#. мало либо много точек +#: tracking.c:523 +msgid "Bad amount of data points" +msgstr "Неверное количество точек с данными" + +#: spots.c:68 +msgid "Bad markers angle" +msgstr "Неверный угол между маркерами" + +#: gtk.c:461 +msgid "Both max values are less than min" +msgstr "Обе максимальные величины меньше минимальной" + +#: imtools.c:217 imtools.c:292 +msgid "Box not found" +msgstr "Область не обнаружена" + +#: tracking.c:535 terrain.c:207 terrain.c:213 +msgid "Can't allocate memory" +msgstr "Не могу выделить память" + +#: imtools.c:445 +msgid "Can't allocate memory for Hough transform" +msgstr "Не могу выделить память для образа Хафа" + +#: imtools.c:356 +msgid "Can't allocate memory for histogram" +msgstr "Не могу выделить память для гистограммы" + +#: spots.c:138 +msgid "Can't allocate memory for new spot" +msgstr "Не могу выделить память для новой точки" + +#: spots.c:89 +msgid "Can't allocate memory for new spots" +msgstr "Не могу выделить память для новых точек" + +#: spots.c:94 +msgid "Can't allocate memory for pointers in spots array" +msgstr "Не могу выделить память для указателей в массиве точек" + +#: opengl.c:425 terrain.c:258 +msgid "Can't allocate memory for texture" +msgstr "Не могу выделить память для текстуры" + +#: tracking.c:57 tracking.c:62 tracking.c:351 +msgid "Can't allocate memory for track points" +msgstr "Не могу выделить память для точек трека" + +#: spots.c:143 +msgid "Can't copy spot to new one" +msgstr "Не могу скопировать точку" + +#: terrain.c:269 +msgid "Can't generate VBO / GL list" +msgstr "Не могу сгенерировать VBO / GL списки" + +#: spots.c:507 +#, fuzzy +msgid "Can't open file" +msgstr "Не могу сохранить файл" + +#: fits.c:287 +msgid "Can't read HDU" +msgstr "Не могу прочесть HDU" + +#: fits.c:192 +msgid "Can't read fits file" +msgstr "Не могу прочесть fits файл" + +#: gtk.c:183 +msgid "Can't save file" +msgstr "Не могу сохранить файл" + +#: spots.c:519 +#, fuzzy +msgid "Can't write to file" +msgstr "Не могу прочесть fits файл" + +#: fitsview.glade:218 +msgid "Choose minimal & maximal spot size" +msgstr "Выберите предельные размеры пятен" + +#: gtk.c:1084 +msgid "Close" +msgstr "Закрыть" + +#: fitsheaders.c:524 +msgid "Close (ctrl+w)" +msgstr "Закрыть (ctrl+w)" + +#: opengl.c:58 +msgid "Color bits" +msgstr "Биты цвета" + +#: open_dialog.c:105 +msgid "Colormap function" +msgstr "Цветовая функция" + +#: fitsheaders.c:54 +msgid "Comment" +msgstr "Комментарий" + +#. (window->context->transformMask & TRANS_TRES) && +#: imtools.c:200 imtools.c:276 +msgid "Define treshold limits first" +msgstr "Определите пороги на гистограмме" + +#: fitsheaders.c:518 +msgid "Delete entry (ctrl+d)" +msgstr "Удалить запись (ctrl+d)" + +#: opengl.c:63 +msgid "Depth bits" +msgstr "Биты глубины" + +#: fitsview.glade:152 +msgid "Draw _tracks" +msgstr "Построение т_реков" + +#: opengl.c:232 +msgid "Empty region" +msgstr "Пустая область" + +#: imtools.c:462 +msgid "Error in Hough transform module" +msgstr "Ошибка в модуле преобразования Хафа" + +#: fitsheaders.c:102 +msgid "Error in value range!" +msgstr "Ошибка в диапазоне значений!" + +#: gtk.c:58 +#, c-format +msgid "Error: %s\n" +msgstr "Ошибка: %s\n" + +#: open_dialog.c:155 +msgid "FITS files" +msgstr "FITS файлы" + +#: fitsview.glade:209 +msgid "Find and enumerate spots" +msgstr "Распознавание пятен" + +#: tracking.c:551 +#, c-format +msgid "Fit gaussian, x0=%.2f I(%.2f, %.2f), s=%.2f, A=%.1f, C=%.3f" +msgstr "Аппроксимация гауссианой, x0=%.2f I(%.2f, %.2f), s=%.2f, A=%.1f, C=" + "%.3f" + +#: fitsheaders.c:133 +msgid "Format error: complex number must be in format" +msgstr "Ошибка формата: комплексное число должно иметь формат" + +#: fitsview.glade:255 +msgid "Identify _circles" +msgstr "Идентификация _окружностей" + +#: fitsview.glade:227 +msgid "Identify _spots" +msgstr "Идентификация _пятен" + +#: imtools.c:348 imtools.c:398 imtools.c:434 +msgid "Image is empty" +msgstr "Изображение не загружено" + +#: open_dialog.c:326 +msgid "Image size" +msgstr "Размер изображения" + +#: fitsheaders.c:121 +msgid "Invalid double number!" +msgstr "Неверное число двойной точности!" + +#. номер первой точки +#. не туда попали? +#: tracking.c:528 +msgid "Invalid number of first point" +msgstr "Неверный номер первой точки" + +#: fitsview.glade:489 +msgid "Linear" +msgstr "Линейный" + +#: fitsview.glade:499 +msgid "Log" +msgstr "Логарифмический" + +#: fitsview.glade:1010 fitsview.glade:1108 +msgid "Max" +msgstr "Максимум" + +#: opengl.c:64 +msgid "Max amount of lights" +msgstr "Максимальное количество источников освещения" + +#: opengl.c:68 +msgid "Max clip planes" +msgstr "Максимальное количество плоскостей отсечения" + +#: gtk.c:464 +msgid "Max height value less than min" +msgstr "Максимальная высота пятна меньше минимальной" + +#: opengl.c:73 +msgid "Max stack depths" +msgstr "Максимальная глубина стека" + +#: opengl.c:67 +msgid "Max texture size" +msgstr "Максимальный размер текстуры" + +#: gtk.c:466 +msgid "Max width value less than min" +msgstr "Максимальная ширина меньше минимальной" + +#: fitsview.glade:978 fitsview.glade:1076 +msgid "Min" +msgstr "Минимум" + +#: fitsview.glade:817 +msgid "Mouse and arrow keys navigation" +msgstr "Навигация мышью и клавиатурой" + +#: fitsview.glade:792 +msgid "Move backward" +msgstr "Назад" + +#: fitsview.glade:776 +msgid "Move down" +msgstr "Вниз" + +#: fitsview.glade:800 +msgid "Move forward" +msgstr "Вперед" + +#: fitsview.glade:768 +msgid "Move left" +msgstr "Влево" + +#: fitsview.glade:760 +msgid "Move right" +msgstr "Вправо" + +#: fitsview.glade:784 +msgid "Move up" +msgstr "Вверх" + +#: fitsheaders.c:54 +msgid "Name" +msgstr "Название" + +#: fitsheaders.c:513 +msgid "New entry (ctrl+n)" +msgstr "Новая запись (ctrl+n)" + +#: imtools.c:213 +msgid "No GSL library found, don't calculate centroids" +msgstr "Не обнаружена библиотека GSL, не вычисляю центроиды" + +#. точек нет? (О_о, а такое вообще может быть?) +#: tracking.c:518 +msgid "No data points" +msgstr "Отсутствуют точки с данными" + +#: terrain.c:199 +msgid "No image in parent window" +msgstr "В родительское окно не загружено изображение" + +#: tracking.c:510 +msgid "No parent window or image" +msgstr "Отсутствует родительское окно или изображение в нем" + +#. "Отсутствуют распознанные пятна" +#: gtk.c:514 +msgid "No recognized spots" +msgstr "Отсутствуют распознанные пятна" + +#: fits.c:356 +msgid "Not an image? (dimensions != 2)" +msgstr "FITS-файл не является изображением? (размерность не равна двум)" + +#: spots.c:67 +msgid "Not enough markers" +msgstr "Недостаточное количество маркеров" + +#: imtools.c:257 +msgid "Not enough points" +msgstr "Недостаточно точек" + +#: fitsheaders.c:486 +msgid "Open fits file first" +msgstr "Вначале откройте fits-файл" + +#: fitsview.glade:55 +msgid "Open in _3D window" +msgstr "Открыть в окне _3D" + +#: fitsview.glade:40 +msgid "Open in _new window" +msgstr "Открыть в _новом окне" + +#: opengl.c:32 +msgid "OpenGL not supported" +msgstr "OpenGL не поддерживается вашей системой" + +#: imtools.c:296 +msgid "Pix not found" +msgstr "Маска не обнаружена" + +#: fitsview.glade:1282 +msgid "Postfocal" +msgstr "Зафокальное" + +#: fitsview.glade:1269 +msgid "Prefocal" +msgstr "Предфокальное" + +#: open_dialog.c:328 +msgid "Preview scale" +msgstr "Масштаб предпросмотра" + +#: open_dialog.c:188 +msgid "Preview settings" +msgstr "Настройки предпросмотра" + +#: open_dialog.c:96 open_dialog.c:327 +msgid "Preview size" +msgstr "Размер предпросмотра" + +#: gtk.c:1081 +msgid "Quit" +msgstr "_Выход" + +#: fitsview.glade:736 +msgid "Rotate X CCW" +msgstr "Вращать против ЧС вокруг оси X" + +#: fitsview.glade:728 +msgid "Rotate X CW" +msgstr "Врощать по ЧС вокруг оси X" + +#: fitsview.glade:744 +msgid "Rotate Z CCW" +msgstr "Вращать против ЧС вокруг оси Z" + +#: fitsview.glade:752 +msgid "Rotate Z CW" +msgstr "Врощать по ЧС вокруг оси Z" + +#: fitsview.glade:243 +msgid "Sa_ve spots" +msgstr "Со_хранить точки" + +#: fitsview.glade:144 +msgid "Select _spots" +msgstr "Выбор _точек" + +#: open_dialog.c:142 +msgid "Select fits file to open" +msgstr "Выберите, какой fits-файл открыть" + +#: terrain.c:177 +msgid "Selected area too small" +msgstr "Выделенная область слишком мала" + +#: gtk.c:358 +msgid "Selected line" +msgstr "Выбрана линия" + +#: tracking.c:475 +#, c-format +msgid "Selected region from %.1f to %.1f, %d datapoints" +msgstr "Выделена область от %.1f до %.1f, %d точек" + +#: opengl.c:241 +msgid "Selected spot: " +msgstr "Выбрано пятно: " + +#: opengl.c:239 +msgid "Selected spot[s]:\n" +msgstr "Выбраны пятна:\n" + +#: opengl.c:243 +msgid "Selected spots: " +msgstr "Выбраны пятна: " + +#: fitsview.glade:115 +msgid "Show _headers" +msgstr "Отобразить _заголовки" + +#: fitsview.glade:107 +msgid "Show _histogram" +msgstr "Отобразить _гистограмму" + +#: fitsview.glade:219 +msgid "Size _tresholds" +msgstr "Ограничение размеров" + +#: fitsview.glade:235 +msgid "So_rt hartmann spots" +msgstr "Сортировать пятна гартманограммы" + +#: spots.c:128 +msgid "Spots array is full, can't place new spot\n" +msgstr "Массив точек полон, не могу добавить новую точку\n" + +#: fitsview.glade:934 +msgid "Spots' size treshold" +msgstr "Ограничение размеров пятен" + +#: opengl.c:64 +msgid "Stencil bits" +msgstr "Биты трафарета" + +#: spots.c:66 +msgid "Too many markers" +msgstr "Слишком много маркеров" + +#: imtools.c:253 +msgid "Too many points" +msgstr "Слишком много точек" + +#: spots.c:69 +msgid "Too many rings" +msgstr "Слишком много колец" + +#. количество записей +#: fitsheaders.c:54 +msgid "Type" +msgstr "Тип" + +#: opengl.c:78 +msgid "VBO is supported\n" +msgstr "Поддерживаются буферы объектов\n" + +#: fitsheaders.c:54 +msgid "Value" +msgstr "Значение" + +#: fitsview.glade:481 +msgid "Y axis scale" +msgstr "Масштаб оси Y" + +#: spots.c:132 +msgid "Zero pointer to spot\n" +msgstr "Нулевой указатель на точку\n" + +#: fitsview.glade:181 +msgid "Zoom _in" +msgstr "У_величить" + +#: fitsview.glade:189 +msgid "Zoom _out" +msgstr "У_меньшить" + +#: fitsview.glade:92 fitsview.glade:466 fitsview.glade:705 +msgid "_Edit" +msgstr "_Изменить" + +#: fitsview.glade:16 fitsview.glade:440 fitsview.glade:671 +msgid "_File" +msgstr "_Файл" + +#: fitsview.glade:818 +msgid "_Game mode" +msgstr "" + +#: fitsview.glade:275 +msgid "_Help" +msgstr "_Помощь" + +#: fitsview.glade:263 +msgid "_Hough transform" +msgstr "Преобразование _Хафа" + +#: fitsview.glade:201 fitsview.glade:517 fitsview.glade:838 +msgid "_Math" +msgstr "_Математика" + +#: fitsview.glade:720 +msgid "_Move" +msgstr "_Переместить" + +#: fitsview.glade:173 fitsview.glade:826 +msgid "_Restore image" +msgstr "_Восстановить масштаб" + +#: fitsview.glade:210 +msgid "_Spots" +msgstr "_Пятна" + +#: fitsview.glade:99 fitsview.glade:473 fitsview.glade:712 +msgid "_View" +msgstr "_Вид" + +#: fitsview.glade:165 +msgid "_Zoom frame" +msgstr "_Увеличение рамкой" + +#: opengl.c:74 +msgid "attrib" +msgstr "атрибутовый" + +#: open_dialog.c:79 +msgid "linear" +msgstr "линейная" + +#: open_dialog.c:79 +msgid "log" +msgstr "логарифмическая" + +#: opengl.c:74 +msgid "modelview" +msgstr "модельно-видовый" + +#: opengl.c:74 +msgid "projection" +msgstr "проекций" + +#: open_dialog.c:79 +msgid "square root" +msgstr "квадратный корень" + +#: opengl.c:75 +msgid "texture" +msgstr "текстур" + +#~ msgid "You sure, you want exit?" +#~ msgstr "Вы уверены, что хотите выйти?" diff --git a/src/locale/ru/update b/src/locale/ru/update new file mode 100755 index 0000000..968e913 --- /dev/null +++ b/src/locale/ru/update @@ -0,0 +1,2 @@ +#!/bin/sh +msgmerge -Uis ru.po messages.po diff --git a/src/open_dialog.c b/src/open_dialog.c new file mode 100644 index 0000000..1a1b129 --- /dev/null +++ b/src/open_dialog.c @@ -0,0 +1,433 @@ +// open_dialog.c - open file dialog with preview +// Copyright: +// Guillaume Chazarain (gliv project) +// 2011 Edward V. Emelianoff (modification for fits open) +// +// This program 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 2 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 this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +// MA 02110-1301, USA. +#include "fitsview.h" +#include "open_dialog.h" + +gchar *saved_path = NULL; + +typedef struct{ + GtkLabel *label; + GtkWidget *image; +} fits_preview; + +GdkPixbuf *getfits(char *filename, gchar **descr); + +void update_preview( GtkFileChooser *file_chooser, + fits_preview *FP){ + GtkWidget *preview = FP->image; + GtkLabel *ilabel = FP->label; + char *filename; + gchar *descr = NULL; + GdkPixbuf *pixbuf = NULL; + filename = gtk_file_chooser_get_preview_filename(file_chooser); + if (filename == NULL) return; + pixbuf = getfits(filename, &descr); + if(pixbuf){ + gint w = gdk_pixbuf_get_width(pixbuf); + w = w - w/4; + if(w < 200) w = 200; + gtk_widget_set_size_request(GTK_WIDGET(ilabel), w, -1); + gtk_label_set_text(ilabel, descr); + } + g_free(filename); + gtk_image_set_from_pixbuf(GTK_IMAGE(preview), pixbuf); + if(pixbuf) g_object_unref(pixbuf); + gtk_file_chooser_set_preview_widget_active(file_chooser, (pixbuf != NULL)); +} + +void cb_changed(GtkComboBox *combo, GtkObject *chooser){ + GtkTreeIter iter; + GtkTreeModel *model; + int val; + gtk_combo_box_get_active_iter(combo, &iter); + model = gtk_combo_box_get_model(combo); + gtk_tree_model_get(model, &iter, 1, &val, -1); + // clear needed config bits + if(val & PREVIEW_SIZEMASK) Global->previewMode &= ~PREVIEW_SIZEMASK; + else if(val & PREVIEW_COLFNMASK) Global->previewMode &= ~PREVIEW_COLFNMASK; + Global->previewMode |= val; // set new + gtk_signal_emit_by_name(chooser, "update-preview"); // redraw +} + +// arguments of funtion which creates menu +enum{ + COMBO_SIZE, + COMBO_COLORFN +}; +GtkWidget *create_combo(unsigned char type, GtkObject *chooser){ + #define SIZES 5 + #define COLORFNS 3 + gchar *sizes[] = {"128 x 128", "256 x 256", "512 x 512", + "768 x 768", "1024 x 1024"}; + gchar *colorfns[] = {N_("linear"), N_("log"), N_("square root")}; + gchar **ptr; + GtkWidget *label = NULL, *vbox = NULL; + gchar *label_text = NULL; + int curpos, i, n; + int startpos = 0; // bit position of appropriate numbers + GtkListStore *store; + GtkWidget *combo; + GtkCellRenderer *cell; + GtkTreeIter iter; + if(type == COMBO_SIZE){ + ptr = sizes; n = SIZES; + if((curpos = Global->previewMode & PREVIEW_SIZEMASK)) + curpos--; + else + curpos = PREVIEW_512 - 1; + startpos = 0; + label_text = g_strdup_printf(_("Preview size")); + } + else if(type == COMBO_COLORFN){ + ptr = colorfns; n = COLORFNS; + if((curpos = (Global->previewMode & PREVIEW_COLFNMASK) >> 3)) + curpos--; + else + curpos = (PREVIEW_SQRT>>3) - 1; + startpos = 3; + label_text = g_strdup_printf(_("Colormap function")); + } + else return NULL; + vbox = gtk_vbox_new(FALSE, 3); + label = gtk_label_new(label_text); + gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0); + store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_INT); + for(i = 0; i < n; i++){ + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, _(ptr[i]), 1, (i+1)<3) r=255; + } + *rgb++ = r; + *rgb++ = g; + *rgb++ = b; +}*/ +void gray2rgb(float gray, guchar *rgb){ + int i = (int)(gray * 4.); + float x = gray - (float)i * .25; + guchar r = 0, g = 0, b = 0; + switch(i){ + case 0: + g = (guchar)(255. * x); + b = 255; + break; + case 1: + g = 255; + b = (guchar)(255. * (1. - x)); + break; + case 2: + r = (guchar)(255. * x); + g = 255; + break; + case 3: + r = 255; + g = (guchar)(255. * (1. - x)); + break; + default: + r = 255; + } + *rgb++ = r; + *rgb++ = g; + *rgb++ = b; +} + +GdkPixbufDestroyNotify free_preview_data(guchar *pixels, gpointer data){ + free(pixels); + free(data); + return FALSE; +} + +// get preview from FITS file filename +GdkPixbuf *getfits(char *filename, gchar **descr){ + gboolean status; + fitsfile *fp; + double (*colorfun)(double); + double linfun(double arg){ return arg; } // bung for PREVIEW_LINEAR + double logfun(double arg){ return log(1.+arg); } // for PREVIEW_LOG + gchar *description = NULL; // some keywords from FITS header + #define TRYFITS(f, ...) \ + do{ status = FALSE; \ + f(__VA_ARGS__, &status); \ + if(status){ \ + free(ima_data); free(pixbuf_data); \ + free(description); free(pix); \ + fits_close_file(fp, &status); \ + return NULL;} \ + }while(0) + void add_keyw(char *keyw){ + char keyval[FLEN_VALUE], *ptr; + if(VALUE_UNDEFINED == fits_read_key(fp, + TSTRING, keyw, keyval, NULL, &status)) return; + if(status) return; + ptr = g_strdup_printf("%s;\t%s=%s", description, keyw, keyval); + free(description); + description = ptr; + } + int MAX_SIZE = 512; // max preview size + unsigned char cntxt; + GdkPixbuf *pixbuf = NULL; + float nullval = 0.; + int i, j, k, l, N, M, stat; + int naxis, w, h, pixScale, Ws, Hs, dtype; + int sz; + cntxt = Global->previewMode & PREVIEW_SIZEMASK; // get preview size + if(cntxt) + switch(cntxt){ + case PREVIEW_128: MAX_SIZE = 128; + break; + case PREVIEW_256: MAX_SIZE = 256; + break; + case PREVIEW_512: MAX_SIZE = 512; + break; + case PREVIEW_768: MAX_SIZE = 768; + break; + case PREVIEW_1024:MAX_SIZE = 1024; + break; + } + // array for preview picture line + float *pix = malloc(MAX_SIZE * sizeof(float)); + long naxes[4]; + float *ima_data = NULL, *ptr, byte, n, m, max, min, wd, avr; + guchar *pptr, *pixbuf_data = NULL; + DBG("Try to open file %s\n", filename); + TRYFITS(fits_open_file, &fp, filename, READONLY); + TRYFITS(fits_get_img_param, fp, 4, &dtype, &naxis, naxes); + if(naxis != 2) return NULL; + w = naxes[0]; + h = naxes[1]; + sz = w * h; + ima_data = malloc(sz * sizeof(float)); + pixbuf_data = malloc(3 * MAX_SIZE * MAX_SIZE * sizeof(guchar)); + + TRYFITS(fits_read_img, fp, TFLOAT, 1, sz, &nullval, ima_data, &stat); + ptr = ima_data; + min = max = *ptr; avr = 0.; + // get statistics: + for(i=0; i max) max = tmp; + else if(tmp < min) min = tmp; + avr += tmp; + } + avr /= (float)sz; + wd = max - min; + i = (int)ceil((float)w / MAX_SIZE); + j = (int)ceil((float)h / MAX_SIZE); + DBG("i=%d, j=%d, ms=%d",i,j,MAX_SIZE); + pixScale = (i > j) ? i : j; // picture scale factor + Ws = w / pixScale; // picture width in pixScale blocks + Hs = h / pixScale; // -//- height pixScale + DBG("w=%d, h=%d, Ws=%d, Hs=%d, pixScale=%d",w,h,Ws,Hs,pixScale); + // prepare a comment to a prewiew: + description = g_strdup_printf( + "%s=%dx%d\n%s=%dx%d\n%s=%.1f%%;\tMax=%g,\tMin=%g,\tAvr=%g", + _("Image size"), w, h, + _("Preview size"), Ws, Hs, + _("Preview scale"), 100./(double)pixScale, + max, min, avr + ); + add_keyw("BITPIX"); + add_keyw("IMAGETYP"); + add_keyw("OBJECT"); + add_keyw("EXPTIME"); add_keyw("EXP"); + add_keyw("AUTHOR"); + add_keyw("DATE"); + M = 0; // line number + for(i = 0; i < Hs; i++){ // cycle through a blocks by lines + //pptr = &pixbuf_data[i * Ws * 3]; + for(j = 0; j < MAX_SIZE; j++) pix[j] = 0; + m = 0.; // amount of strings read in block + for(l = 0; l < pixScale; l++, m++){ // cycle through a block lines + ptr = &ima_data[M * w]; + N = 0; // number of column + for(j = 0; j < Ws; j++){ // cycle through a blocks by columns + n = 0.; // amount of columns read in block + byte = 0.; // average intensity in block + for(k = 0; k < pixScale; k++, n++){ // cycle through block pixels + if(N++ < w) // row didn't end + byte += *ptr++; // sum[(pix-min)/wd]/n = [sum(pix)/n-min]/wd + else break; + } + pix[j] += byte / n;//(byte / n - min)/wd; + } + if(++M >= h) break; + } + // fill unused picture pixels + ptr = &ima_data[i*Ws]; + for(l = 0; l < Ws; l++) + *ptr++ = pix[l] / m; + } + ptr = ima_data; + sz = Ws * Hs; + max = min = *ptr; + avr = 0; + for(i=0; i < sz; i++, ptr++){ + float tmp = *ptr; + if(tmp > max) max = tmp; + else if(tmp < min) min = tmp; + avr += tmp; + } + avr /= (float)sz; + wd = max - min; + avr = (avr - min) / wd; // normal average by preview + avr = -log(avr); // scale factor + if(avr > 1.) wd /= avr; + ptr = ima_data; + colorfun = sqrt; + if((Global->previewMode & PREVIEW_COLFNMASK) == PREVIEW_LINEAR) + colorfun = linfun; + else if((Global->previewMode & PREVIEW_COLFNMASK) == PREVIEW_LOG) + colorfun = logfun; + for(i = Hs - 1; i > -1; i--){// fill pixbuf mirroring image by vertical + pptr = &pixbuf_data[Ws * i * 3]; + for(j = 0; j < Ws; j++){ + gray2rgb(colorfun((*ptr++ - min) / wd), pptr); + pptr += 3; + } + } + fits_close_file(fp, &status); + pixbuf = gdk_pixbuf_new_from_data( + pixbuf_data, // guchar* data + GDK_COLORSPACE_RGB, // only this supported + FALSE, // no alpha + 8, // number of bits in byte (WTF? who is this idiot?) + Ws, Hs, // size + Ws * 3, // line length in bytes + (GdkPixbufDestroyNotify)free_preview_data, // function (*GdkPixbufDestroyNotify) (guchar *pixels, gpointer data); + (gpointer)description // pointer data + ); + free(ima_data); + free(pix); + *descr = description; + DBG("OK, preview is ready, previewMode = %d", Global->previewMode); + return pixbuf; +} diff --git a/src/opengl.c b/src/opengl.c new file mode 100644 index 0000000..d5f1899 --- /dev/null +++ b/src/opengl.c @@ -0,0 +1,731 @@ +// opengl.c - functions to work with openGL +// +// Copyright 2011 Edward V. Emelianoff +// +// This program 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 2 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 this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +// MA 02110-1301, USA. +#include "opengl.h" +#include "spots.h" +#include "imtools.h" +#include "contours.h" +#include "open_dialog.h" + +#define BUFSIZE 512 // select buffer sizw +GLdouble ObjX = 0., ObjY = 0.; // mouse click coordinates (in window CS) +gboolean forcesel = FALSE; +gboolean useVBO = FALSE; // use VBO or lists + +// these functions are absent in headers +void glBlendEquation(GLenum); +void glBlendFuncSeparate(GLenum, GLenum, GLenum, GLenum); + +#define BADOGL() do{g_err(_("OpenGL not supported")); \ + g_assert_not_reached();}while(0) + +gboolean queryExtension(char *extName){ + char *p = (char *)glGetString(GL_EXTENSIONS); + char *end = p + strlen(p); + while(p < end){ + size_t n = strcspn(p, " "); + if((strlen(extName)==n) && (strncmp(extName,p,n)==0)) + return TRUE; + p += (n + 1); + } + return FALSE; +} + +void getGLinfo(GtkWidget *Area){ + GLint a,b,c,d; + GdkGLDrawable *glDrawable = gtk_widget_get_gl_drawable(Area); + GdkGLContext *glContext = gtk_widget_get_gl_context(Area); + if (!gdk_gl_drawable_gl_begin(glDrawable, glContext)) + g_assert_not_reached(); + g_print(_("\nOpenGL info:\n")); + glGetIntegerv(GL_RED_BITS, &a); + glGetIntegerv(GL_GREEN_BITS, &b); + glGetIntegerv(GL_BLUE_BITS, &c); + glGetIntegerv(GL_ALPHA_BITS, &d); + g_print("%s (R, G, B, alpha) = (%d, %d, %d, %d)\n", _("Color bits"), + a, b, c, d); + glGetIntegerv(GL_DEPTH_BITS, &a); + glGetIntegerv(GL_STENCIL_BITS, &b); + glGetIntegerv(GL_MAX_LIGHTS, &c); + g_print("%s = %d, %s = %d, %s = %d\n", _("Depth bits"), a, + _("Stencil bits"), b, _("Max amount of lights"), c); + glGetIntegerv(GL_MAX_TEXTURE_SIZE, &a); + glGetIntegerv(GL_MAX_CLIP_PLANES, &b); + g_print("%s = %d, %s = %d\n", _("Max texture size"), a, + _("Max clip planes"), b); + glGetIntegerv(GL_MAX_MODELVIEW_STACK_DEPTH, &a); + glGetIntegerv(GL_MAX_PROJECTION_STACK_DEPTH, &b); + glGetIntegerv(GL_MAX_ATTRIB_STACK_DEPTH, &c); + glGetIntegerv(GL_MAX_TEXTURE_STACK_DEPTH, &d); + g_print("%s: %s = %d, %s = %d, %s = %d, %s = %d\n", _("Max stack depths"), + _("modelview"), a, _("projection"), b, _("attrib"), c, + _("texture"), d); + //g_print("%s: %s\n", _("All extensions"), (char *)glGetString(GL_EXTENSIONS)); + useVBO = queryExtension("GL_ARB_vertex_buffer_object"); + if(useVBO) g_print(_("VBO is supported\n")); + gdk_gl_drawable_gl_end(glDrawable); +} + +void freeGLmemory(Window *window){ + if(!window->texture) return; + GLuint *tex = &window->texture->tex; + if(!*tex) return; + if(useVBO) + glDeleteBuffersARB(1, tex); + else + glDeleteLists(*tex, 1); +} + +/* + * Coordinates transformation from CS of drawingArea into CS of picture + * x,y - pointer coordinates + * X,Y - coordinates of appropriate point at picture + */ +void conv_mouse_to_image_coords(double x, double y, + double *X, double *Y, + Window *window){ + double a = window->Daspect / window->Zoom; + *X = x * a - window->mouse.x; + *Y = window->mouse.y - y * a; +} + +void conv_image_to_mouse_coords(double X, double Y, + double *x, double *y, + Window *window){ + double a = window->Zoom / window->Daspect; + *x = (X + window->mouse.x) * a; + *y = (window->mouse.y - Y) * a; +} + +gboolean configure(GtkWidget *Area, GdkEventExpose *event, Window *window){ + //FNAME(); + double a, A, W, H, w, h; + double Zoom = window->Zoom; + if(event) + if(event->count) return FALSE; + TEXTURE *texture = (TEXTURE*)window->texture; + GdkGLDrawable *glDrawable = gtk_widget_get_gl_drawable(Area); + GdkGLContext *glContext = gtk_widget_get_gl_context(Area); + if (!gdk_gl_drawable_gl_begin(glDrawable, glContext)) + g_assert_not_reached(); + gdk_gl_drawable_wait_gdk(glDrawable); + // set position + double W0, H0, x0,y0, xm,ym; + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + W = W0 = Area->allocation.width; + H = H0 = Area->allocation.height; + A = W / H; + glViewport(0, 0, W, H); + if(texture){ + // compute the right dimensions for rectangle with picture + w = texture->w; h = texture->h; + a = w / h; w /= 2.; h /= 2.; + if(A > a){ + W = h * A; H = h; + //window->mouse.rx = (W0 - H0*a)/2.; window->mouse.ry = 0.; + window->Daspect = h / H0 * 2.; + } + else{ + H = w / A; W = w; + //window->mouse.ry = (H0 - W0/a)/2.; window->mouse.rx = 0.; + window->Daspect = w / W0 * 2.; + } + // recalculate limits for the rulers + x0 = W/Zoom - w + window->move.x / Zoom; + window->mouse.x = x0; + y0 = H/Zoom - h + window->move.y / Zoom; + xm = 2. * W / Zoom - x0; + ym = 2. * H / Zoom - y0; + window->mouse.y = ym; + //DBG("W=%g, w=%g, Zoom=%g, x0=%g, xm=%g, Da=%g", W, w, Zoom, x0, xm, Daspect); + set_Drulers(x0, y0, xm, ym, window); + glOrtho(-W,W, -H,H, -1., 1.); + } + glMatrixMode(GL_MODELVIEW); + glBlendEquation(GL_FUNC_ADD); + glEnable(GL_BLEND); + gdk_gl_drawable_gl_end(glDrawable); + refresh_state(window); + return FALSE; +} + +/* + * Ellipse with center at (x,y) and radii (rx,ry) + * plots as polyline with stride of 10 degr + */ +void draw_ellipce(GLfloat x, GLfloat y, GLfloat rx, GLfloat ry){ + GLfloat pi2 = M_PI * 2., step = pi2 / 36., angle; + glBegin(GL_LINE_LOOP); + for(angle = 0.; angle < pi2; angle += step) + glVertex2f(x+rx*cos(angle), y+ry*sin(angle)); + glEnd(); +} + +/* + * Graphical indication of recognized spots + * window - the window with picture + * w, h - half-width & half-height of picture (texture) + * CX0, CY0 - zero point coordinates in picture CS (in center) + * box == TRUE - draw region, == FALSE - set selection region + */ +void draw_squares(Window *window, double w, double h, double CX0, double CY0, gboolean box){ + int i, n; + double x0, y0, x1, y1; + BOX *bbox; Spot **spot; Coordinates *crds; + Spots *spots = window->spots; + if(!spots) return; + n = spots->n; + spot = spots->spot; + glPushMatrix(); + for(i = 0; i < n; i++){ + bbox = &spot[i]->box; + crds = &spot[i]->c; + x0 = bbox->x - w; y0 = h - bbox->y; + x1 = x0 + bbox->w; y1 = y0 - bbox->h; + if(box){ + glColor3f(0., 1., 0.); // green is box borders + glBegin(GL_LINE_LOOP); + glVertex2f(x0, y0); glVertex2f(x1, y0); + glVertex2f(x1, y1); glVertex2f(x0, y1); + glEnd(); + if(window->context->transformMask & TRANS_HARTMANN){ + x0 = crds->x + CX0; + y0 = crds->y + CY0; + glColor3f(1., 0., 0.); // red is spot half-widths + draw_ellipce(x0, y0, crds->rx, crds->ry); + //glColor3f(0., 1., 0.); + } + }else{ // box == FALSE - set frames for selection + glLoadName(i); + glRectf(x0, y0, x1, y1); + } + } + if(box){ // draw cross - CS zero + glColor3f(1., 0., 1.); + glBegin(GL_LINES); + glVertex2f(CX0-15., CY0-15.); glVertex2f(CX0+15., CY0+15.); + glVertex2f(CX0-15., CY0+15.); glVertex2f(CX0+15., CY0-15.); + glEnd(); + } + glPopMatrix(); +} + +/* + * graphical indication of isolines + * window - the window + * w, h - half-width & half-height of picture (texture) + * CX0, CY0 - zero point coordinates in picture CS (in center) + * _2D == TRUE for 2D images + */ +void draw_isolines(Window *window, double w, double h, gboolean _2D){ + //FNAME(); + IMAGE *ima = window->image; + if(!ima) return; + Contour *c; + cPoint *p; + cList **cl = window->image->contours; + int i, n = window->image->Ncontours; + if(!cl || n < 1) return; + float m = ima->stat.min, wd = ima->stat.max - m; + float *lvls = ima->cLevels, curLevel; + guchar colr[3] = {255,0,0}; // isolines' colors + glPushMatrix(); + for(i = 0; i < n; i++, cl++){ // cycle by isolines + if(!*cl) continue; // given level doesn't have isolines + c = (*cl)->first; // first contour for given level + if(!c || (*cl)->N < 1) continue; + gray2rgb((lvls[(*cl)->L]-m)/wd, colr); // convert level into color + glColor3ubv(colr); + if(_2D) curLevel = 0.; + else curLevel = lvls[(*cl)->L] * ((GLfloat)w + (GLfloat)h) / 2./ wd; + //DBG("curLevel=%g, gray=%g, color=(%d,%d,%d)", curLevel, (lvls[(*cl)->L]-m)/wd, colr[0],colr[1],colr[2]); + while(c){ // cycle by contours + p = c->first; + if(!p || c->N < 1){ c = c->next; continue;} + if(c->closed) + glBegin(GL_LINE_LOOP); + else + glBegin(GL_LINE_STRIP); + while(p){ // cycle by contour points + glVertex3f(p->x-w, p->y-h, curLevel); + p = p->next; + } + glEnd(); + c = c->next; + } + } + glPopMatrix(); +} + +void processHits(Window *window, GLint hits, GLuint buffer[]){ + unsigned int i, j; + GLuint names, *ptr; + Spot *spot; + //Coordinates crds; + gchar buf[256]; + int l = 255; + Spots *spots = window->spots; + if(!spots) return; + printf ("hits = %d\n", hits); + ptr = (GLuint *) buffer; + if(hits < 1){ + set_status_text(StatusText, _("Empty region"), window); + return; + } + for (i = 0; i < (unsigned int)hits; i++){ + names = *ptr; + DBG("number of names for this hit = %d\n", names); + ptr += 3; + g_print(_("Selected spot[s]:\n")); + if(names == 1) + g_sprintf(buf, _("Selected spot: ")); + else + g_sprintf(buf, _("Selected spots: ")); + l -= strlen(buf); + for (j = 0; j < names; j++){ + if(*ptr < (GLuint)spots->n){ + spot = spots->spot[*ptr]; + g_print("id: %d, xC=%.2f, yC=%.2f, r=%.2f, phi=%.2f, ", + spot->id, spot->xC, spot->yC, spot->r, spot->phi); + g_print("cn: (%.1f, %.1f), dr:(%.1f, %.1f)\n", + spot->c.x, spot->c.y, spot->c.rx, spot->c.ry); + g_print("box X: [%d, %d], box Y: [%d, %d], cn: [%d, %d]\n", + spot->box.x, spot->box.x+spot->box.w, + spot->box.y, spot->box.y+spot->box.h, + spot->box.x+spot->box.w/2, spot->box.y+spot->box.h/2); + /* get_gaussian_center(&spot->box, &crds); + g_print("\tCenter: (%.1f, %.1f), wd: (%.1f, %.1f)", + crds.x, (double)image->height-crds.y-1., crds.rx, crds.ry); + */ + l -= g_snprintf(&buf[strlen(buf)], l, "%d ", spot->id); + } + ptr++; + } + g_print("\n"); + set_status_text(StatusText, buf, window); + } +} + +gboolean expose(GtkWidget *Area, GdkEventExpose *event, Window *window){ + TEXTURE *texture = (TEXTURE*)window->texture; + double w, h; + GLfloat tred[4] = {1.,0.,0.,1.}; + GLfloat twhite[4] = {1.,1.,1.,1.}; + if(event) + if(event->count) return FALSE; + //FNAME(); + GdkGLDrawable *glDrawable = gtk_widget_get_gl_drawable(Area); + GdkGLContext *glContext = gtk_widget_get_gl_context(Area); + if (!gdk_gl_drawable_gl_begin(glDrawable, glContext)) + g_assert_not_reached(); + glClearColor(.1, .1, .1, 1.0); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glColor3fv(tred); + if(texture){ + w = texture->w / 2.; h = texture->h / 2.; + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + glTranslatef(window->move.x, window->move.y, 0.); + glScalef(window->Zoom, window->Zoom, 1.); + //DBG("moveXY=%g, %g; Zoom=%g, texture: id=%d, w=%d", + // window->move.x, window->move.y, window->Zoom, texture->tex, texture->w); + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, texture->tex); + //DBG("texture: %d", texture->tex); + //DBG("w=%g, h=%g\n", w, h); + glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, twhite); + glDisable(GL_BLEND); + glBegin(GL_QUADS); + glTexCoord2f(0.0f, 0.0f); glVertex2f(-w, -h ); + glTexCoord2f(1.0f, 0.0f); glVertex2f( w, -h ); + glTexCoord2f(1.0f, 1.0f); glVertex2f( w, h ); + glTexCoord2f(0.0f, 1.0f); glVertex2f(-w, h ); + glEnd(); + glDisable(GL_TEXTURE_2D); + /*glBegin(GL_LINE_LOOP); + glTexCoord2f(0.0f, 0.0f); glVertex2f(-w, -h ); + glTexCoord2f(1.0f, 0.0f); glVertex2f( w, -h ); + glTexCoord2f(1.0f, 1.0f); glVertex2f( w, h ); + glTexCoord2f(0.0f, 1.0f); glVertex2f(-w, h ); + glEnd();*/ + glEnable(GL_BLEND); + + // draw addition information + if(window->context->visualMode & + (SHOW_POTBOXES|SHOW_POTSELECT|SHOW_ISOLINES)){ + GLuint selectBuf[BUFSIZE]; + GLint hits; + double CX0 = AxisX - w, CY0 = h - AxisY; + gboolean showiso = window->context->visualMode & SHOW_ISOLINES; + gboolean showbox = window->context->visualMode & SHOW_POTBOXES; + gboolean showsel = window->context->visualMode & SHOW_POTSELECT; + GLint viewport[4]; + if(showsel && forcesel){ // get coordinates from selection buffer + glPushMatrix(); + glGetIntegerv(GL_VIEWPORT, viewport); + glSelectBuffer(BUFSIZE, selectBuf); + glRenderMode(GL_SELECT); + glInitNames(); + glPushName(0); + glMatrixMode(GL_PROJECTION); + glPushMatrix(); + glLoadIdentity(); + /* create pixel picking region near cursor location */ + DBG("x=%g, y=%g", ObjX, ObjY); + gluPickMatrix(ObjX, (viewport[3] - ObjY), 5.0, 5.0, viewport); + //gluPickMatrix(ObjX, ObjY, 10.0, 10.0, viewport); + double W = Area->allocation.width; + double H = Area->allocation.height; + double A = W / H; + if(A > w / h){ + W = h * A; H = h;} + else{ + H = w / A; W = w;} + glOrtho(-W,W, -H,H, -1., 1.); + glMatrixMode(GL_MODELVIEW); + if(showbox) draw_squares(window, w, h, CX0, CY0, FALSE); + if(showiso) draw_isolines(window, w, h, TRUE); + glMatrixMode(GL_PROJECTION); + glPopMatrix(); + hits = glRenderMode(GL_RENDER); + processHits(window, hits, selectBuf); + forcesel = FALSE; + glPopMatrix(); + } + glMatrixMode(GL_MODELVIEW); + if(showbox) draw_squares(window, w, h, CX0, CY0, TRUE); + if(showiso) draw_isolines(window, w, h, TRUE); + } +/* + // layers mix ===> + glLoadIdentity(); + //glTranslatef(moveX,moveY,0); + glScalef(window->Zoom, window->Zoom, 1.); + glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, tred); + glBlendFunc(GL_ONE, GL_ONE); + glEnable(GL_TEXTURE_2D); + glBegin(GL_QUADS); + glTexCoord2f(0.0f, 0.0f); glVertex2f(-w, -h ); + glTexCoord2f(1.0f, 0.0f); glVertex2f( w, -h ); + glTexCoord2f(1.0f, 1.0f); glVertex2f( w, h ); + glTexCoord2f(0.0f, 1.0f); glVertex2f(-w, h ); + glEnd(); + glDisable(GL_TEXTURE_2D); + // <==== +*/ + } + else DBG("No texture"); + gdk_gl_drawable_swap_buffers(glDrawable); + gdk_gl_drawable_wait_gl(glDrawable); + gdk_gl_drawable_gl_end(glDrawable); + return FALSE; +} + +/* +gboolean idle(gpointer userData){ + GtkWidget *Area = GTK_WIDGET(userData); + gdk_window_invalidate_rect(Area->window, &Area->allocation, FALSE); + return TRUE; +}*/ + +void initGl(GtkWidget *Area){ + FNAME(); + GdkGLConfig *glConfig; + GdkGLConfigMode mode = GDK_GL_MODE_RGB | + GDK_GL_MODE_DEPTH | + GDK_GL_MODE_ALPHA | + GDK_GL_MODE_DOUBLE; + if(!gdk_gl_query_extension()) + BADOGL(); + glConfig = gdk_gl_config_new_by_mode(mode); + if(!glConfig) + glConfig = gdk_gl_config_new_by_mode(mode & ~GDK_GL_MODE_ALPHA); + if(!glConfig) + BADOGL(); + if(!gtk_widget_set_gl_capability(Area, glConfig, NULL, TRUE, + GDK_GL_RGBA_TYPE)) + BADOGL(); +} + +/* + * generation of textures to show in Area widget of image + * if redraw == TRUE texture substitutes previous otherwise generate the new one + */ +void gen_texture(IMAGE *image, Window *window, gboolean redraw){ + //~ FNAME(); + TEXTURE *texture = window->texture; + GtkWidget *Area = window->drawingArea; + GdkGLContext *glContext = gtk_widget_get_gl_context(Area); + GdkGLDrawable *glDrawable = gtk_widget_get_gl_drawable(Area); + if (!gdk_gl_drawable_gl_begin(glDrawable, glContext)) + g_assert_not_reached(); + glEnable(GL_TEXTURE_2D); + if(!texture){ + INIT(window->texture, TEXTURE); + texture = window->texture; + if(!texture){ + g_err(_("Can't allocate memory for texture")); + return; + } + //~ g_print(_("Generating texture\n")); + glGenTextures(1, &texture->tex); + //~ DBG("%s, tex=%d\n", gluErrorString(glGetError()), texture->tex); + } + //~ DBG("Image size: w=%d, h=%d", image->width, image->height); + glBindTexture(GL_TEXTURE_2D, texture->tex); + if(image && image->data){ + int i,j, k, w = image->width, h = image->height; + GLfloat *tex = NULL; + GLfloat min = image->stat.min, wd = image->stat.max - min; + GLfloat *ptri = image->data, *ptro; + DBG("min = %g, wd = %g", min, wd); + if(image->stat.max > 1. || min < 0.){ + tex = malloc(w * h * sizeof(GLfloat)); + ptro = tex; + #ifdef EBUG + double t0 = dtime(); + #endif + #pragma omp parallel for + for(i=0; idata; + if(redraw) + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, + GL_LUMINANCE, GL_FLOAT, ptro); // s/image->data/tex/ + else + glTexImage2D(GL_TEXTURE_2D, 0, GL_INTENSITY, w, h, + 0, GL_LUMINANCE, GL_FLOAT, ptro); + //DBG("%s\n", gluErrorString(glGetError())); + //DBG("W: %d, H:%d, im[100]=%f\n", image->width, image->height, image->data[100]); + texture->w = image->width; texture->h = image->height; + free(tex); + } + else{ + DBG("Empty texture"); + glTexImage2D(GL_TEXTURE_2D, 0, GL_INTENSITY, 10., 10., + 0, GL_INTENSITY, GL_FLOAT, NULL); + } + glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); + glTexEnvf(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_TEXTURE); + glTexEnvf(GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_CONSTANT); + //glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST); + //glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST); + glDisable(GL_TEXTURE_2D); + gdk_gl_drawable_gl_end(glDrawable); +/* + * Events: + * expose - redraw part of an image + * configure - redraw window when size/position changed + * if handler returns TRUE signal processing stops + * otherwise it can be process by other handlers + */ + g_signal_connect(Area, "expose-event", + G_CALLBACK(expose), window); + g_signal_connect(Area, "configure-event", + G_CALLBACK(configure), window); + //~ DBG("end"); + force_redraw(Area); +} + +void force_redraw(GtkWidget *Area){ + gtk_widget_queue_resize(Area); + gtk_widget_queue_draw(Area); +} + +void pickRegion(double x, double y){ + FNAME(); + forcesel = TRUE; + ObjX = x; + ObjY = y; +} + +gboolean configure3D(GtkWidget *Area, GdkEventExpose *event, Window *window){ + FNAME(); + GdkGLContext *glContext = gtk_widget_get_gl_context(Area); + GdkGLDrawable *glDrawable = gtk_widget_get_gl_drawable(Area); + double w, h; + GLfloat ratio; +if(!window)return FALSE; + //~ GLfloat H = (GLfloat)window->texture->h; + if(event) + if(event->count) return FALSE; + if (!gdk_gl_drawable_gl_begin(glDrawable, glContext)) + g_assert_not_reached(); + w = Area->allocation.width; + h = Area->allocation.height; + ratio = w / h; + glViewport(0, 0, w, h); + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + gluPerspective(45., ratio, 0.1, 1e10); + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + gdk_gl_drawable_gl_end(glDrawable); + return FALSE; +} + +gboolean expose3D(GtkWidget *Area, GdkEventExpose *event, Window *window){ + FNAME(); + TEXTURE *texture = window->texture; + GdkGLContext *glContext = gtk_widget_get_gl_context(Area); + GdkGLDrawable *glDrawable = gtk_widget_get_gl_drawable(Area); + GLfloat H = (GLfloat)window->image->height; + if(event) + if(event->count) return FALSE; + if (!gdk_gl_drawable_gl_begin(glDrawable, glContext)) + g_assert_not_reached(); + GLfloat spotDir[] = {0.,0.,-1.,1.}; + GLfloat pos[] = {0.,0.,0.,1.}; + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glLoadIdentity(); + glLightfv(GL_LIGHT1,GL_POSITION,pos); + glLightfv(GL_LIGHT1,GL_SPOT_DIRECTION, spotDir); + if(window->context->drawingMode == GAME_MODE){ // mouse navigation + GLfloat lx, ly, lz, za = window->Zangle, xa = window->Xangle; + GLfloat x = window->move.x, y = window->move.y, z = H * window->Zoom; + lx = cosf(za)*sinf(xa); + ly = -sinf(za); + lz = -cosf(za)*cosf(xa); + gluLookAt(x, y, z, + x+lx, y+ly, z+lz, + 0., 1., 0.); + //~ GLfloat w = Area->allocation.width, h = Area->allocation.height; + //GLfloat z = H * window->Zoom; + //~ GLfloat yangle = 180. / M_PI * atan2f(window->Xangle - w/2., z); + //~ GLfloat xangle = 180. / M_PI * atan2f(window->Zangle - h/2., z); + //~ glRotatef(yangle, 0.,1.,0.); + //~ glRotatef(xangle, 1.,0.,0.); + //~ glTranslatef(window->move.x, 0., -z); + //glRotatef(window->Xangle, 0.,1.,0.); + //~ glRotatef(window->Zangle, 1.,0.,0.); + //glRotatef(window->Zangle, cosf(window->Xangle*M_PI/180.),0.,sinf(window->Xangle*M_PI/180.)); + //glTranslatef(window->move.x, 0., -z); + }else{ // key navigation + glTranslatef(window->move.x, window->move.y, -H * window->Zoom); + glRotatef(window->Xangle, 1.,0.,0.); + glRotatef(window->Zangle, 0.,0.,1.); + } + glPushMatrix(); + glEnable(GL_LIGHTING); + if(useVBO){ + GLint y, w = window->image->width, h = window->image->height; + GLint pts = w*2; + size_t sz = w * h * 3 * sizeof(GLfloat); + glBindBufferARB(GL_ARRAY_BUFFER_ARB, texture->tex); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, texture->w); + glEnableClientState(GL_NORMAL_ARRAY); + glEnableClientState(GL_VERTEX_ARRAY); + glEnableClientState(GL_INDEX_ARRAY); + glNormalPointer(GL_FLOAT, 0, (GLfloat*)sz); + glIndexPointer(GL_UNSIGNED_INT, 0, 0 ); + glVertexPointer(3, GL_FLOAT, 0, 0); + h--; + sz = 0; w = pts * sizeof(GLfloat); + for(y = 0; y < h; y++, sz += w) + glDrawElements(GL_TRIANGLE_STRIP, pts, GL_UNSIGNED_INT, (GLvoid*)(sz)); + glDisableClientState(GL_VERTEX_ARRAY); + glDisableClientState(GL_NORMAL_ARRAY); + glDisableClientState(GL_INDEX_ARRAY); + glBindBufferARB(GL_ARRAY_BUFFER_ARB, 0); + }else + glCallList(texture->tex); + glPushMatrix(); + glDisable(GL_LIGHTING); + glLineWidth(3.0); + draw_isolines(window, ((double)window->image->width)/2.+.5, + ((double)window->image->height)/2.+.5, FALSE); + /*glColor3f(0.,0.,1.); + glBegin(GL_QUADS); + glVertex3f(-100.f, -100.f, -12.f ); + glVertex3f(-100.f, 100.f, -12.f ); + glVertex3f( 100.f, 100.f, -12.f ); + glVertex3f( 100.f, -100.f, -12.f ); + glEnd(); + glBegin(GL_LINES); + glVertex3fv(pos); + glColor3f(1.f,0.f,0.f); + glVertex3fv(spotDir); + glEnd();*/ + glPopMatrix(); + gdk_gl_drawable_swap_buffers(glDrawable); + gdk_gl_drawable_wait_gl(glDrawable); + gdk_gl_drawable_gl_end(glDrawable); + return FALSE; +} + +void init3D(Window *window){ + FNAME(); + GtkWidget *Area = window->drawingArea; + GdkGLContext *glContext = gtk_widget_get_gl_context(Area); + GdkGLDrawable *glDrawable = gtk_widget_get_gl_drawable(Area); + GLfloat lAmbient[] = {0.3,0.3,0.3,1.0}; + GLfloat lDiffuse[] = {1.0,1.0,1.0,1.0}; + GLfloat lSpecular[]= {1.0,1.0,1.0,1.0}; + GLfloat l2Ambient[] = {0.0,0.0,0.1,1.0}; + GLfloat l2Diffuse[] = {0.0,0.0,0.3,1.0}; + GLfloat l2Specular[]= {0.0,0.0,0.5,1.0}; + GLfloat mShininess[] = {110.0}; + GLfloat mSpecular[] = {0.8,0.8,1.0,1.0}; + GLfloat mDiffuse[] = {0.8,0.8,1.0,1.0}; + GLfloat mAmbient[] = {0.2,0.2,0.2,1.0}; + if (!gdk_gl_drawable_gl_begin(glDrawable, glContext)) + g_assert_not_reached(); + glEnable(GL_RESCALE_NORMAL); + glEnable(GL_DEPTH_TEST); + //~ glEnable(GL_CULL_FACE); + //~ glCullFace(GL_BACK); + GLfloat spotDir[] = {0.,0.,-1.,1.}; + GLfloat pos[] = {0.,0.,0.,1.}; + glLightfv(GL_LIGHT1,GL_POSITION,pos); + glLightfv(GL_LIGHT1,GL_SPOT_DIRECTION, spotDir); + glLightf(GL_LIGHT1,GL_SPOT_CUTOFF, 90.); + glLightf(GL_LIGHT1,GL_SPOT_EXPONENT, 20.); + glLightfv(GL_LIGHT0,GL_AMBIENT,l2Ambient); + glLightfv(GL_LIGHT0,GL_DIFFUSE,l2Diffuse); + glLightfv(GL_LIGHT0,GL_SPECULAR,l2Specular); + glLightfv(GL_LIGHT1,GL_AMBIENT,lAmbient); + glLightfv(GL_LIGHT1,GL_DIFFUSE,lDiffuse); + glLightfv(GL_LIGHT1,GL_SPECULAR,lSpecular); + glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER,GL_TRUE); + glMaterialfv(GL_FRONT, GL_SPECULAR, mSpecular); + glMaterialfv(GL_FRONT, GL_SHININESS,mShininess); + glMaterialfv(GL_FRONT, GL_AMBIENT, mAmbient); + glMaterialfv(GL_FRONT, GL_DIFFUSE, mDiffuse); + //~ glMaterialfv(GL_BACK, GL_SPECULAR, mSpecular); + //~ glMaterialfv(GL_BACK, GL_SHININESS,mShininess); + //~ glMaterialfv(GL_BACK, GL_AMBIENT, mGray); + //~ glMaterialfv(GL_BACK, GL_DIFFUSE, mWhite); + glEnable(GL_LIGHTING); + glEnable(GL_LIGHT0); + glEnable(GL_LIGHT1); + gdk_gl_drawable_gl_end(glDrawable); + g_signal_connect(Area, "expose-event", + G_CALLBACK(expose3D), window); + g_signal_connect(Area, "configure-event", + G_CALLBACK(configure3D), window); + force_redraw(Area); +} diff --git a/src/scripts/genh b/src/scripts/genh new file mode 100755 index 0000000..c0395bf --- /dev/null +++ b/src/scripts/genh @@ -0,0 +1,3 @@ +#!/bin/sh +echo "const gchar *ui = `sed -r 's/^[[:space:]]*//;s/\"/\\\\\"/g;s/(^.*$$)/\"&\"/' $1` ;" > ui.h +echo "Wrote ui.h" diff --git a/src/spots.c b/src/spots.c new file mode 100644 index 0000000..387311a --- /dev/null +++ b/src/spots.c @@ -0,0 +1,534 @@ +// spots.c - functions to work with spots (sorting, enumerating & so on) +// +// Copyright 2011 Edward V. Emelianoff +// +// This program 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 2 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 this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +// MA 02110-1301, USA. + +/* + * Since our image is mirrored, to establish the direction of the axis + * of symmetry it is necessary to know the position angle of the camera, etc. + * It is easier to work in a "mirror" coordinates. Similarly, the coordinates + * of the mask should also be given the "mirror". + */ +#include "gtk.h" +#include "spots.h" +#include "gauss.h" + +const double M_2PI = 2. * M_PI; +// about a quarter of 11.25 degrees - the threshold for the identification of radial points and frames +const double PHI_STEP = 0.05; +// angle between the markers (for identification), 56.25 degrees +const double DPHI_MARKERS = 0.98; +const double aStep = M_PI / 16.; // 11.25 degrees, the angle between adjacent rays +const int N_ANGLES = 16; // number of lines on hartmanogramm (half of number of rays) +const int N_RINGS = 8; // number of rings on hartmanogramm + +// angle of the Y-axis of the matrix relative to the Y-axis mask +double SlopeAngle; +// angular position of the marker (near the center) for a zero slope, -101.25 degrees +const double Slope0 = -1.767146; + +/* + * these constants are the position of the axis of rotation of the table. + * In the coordinate system of the image (ie, the Y axis goes from top to bottom) + * means that it coincides with the optical axis. Naturally, in the work + * program these coordinates should be calculated + */ +double AxisX = 1523., AxisY = 1533.; + + +// declaration of the flags of errors leading to the exit from auto mode +enum{ + TOO_MANY_MARKERS // discovered more than two markers + ,NOT_ENOUGH_MARKERS // not found the two markers + ,BAD_MARKERS_ANGLE // angle between the markers over DPHI_MARKERS +- PHI_STEP + ,TOO_MANY_RINGS // found more rings than it should be +}; + +// messages appropriate to flags +char *errmsg[] = { + N_("Too many markers") + ,N_("Not enough markers") + ,N_("Bad markers angle") + ,N_("Too many rings") +}; + +#ifdef LEPTONICA_FOUND +/* + * Violated any restrictions on the automatic mode + * In theory the function should exit the batch mode with the mapping errors + */ +void bad_circumstances(int errcode){ + red("\nerror, need to stop\n"); + g_err(_(errmsg[errcode])); +} + +// memory allocation for n spots array +Spots* spots_alloc(int n){ + Spots *ptr = malloc(sizeof(Spots)); + if(!ptr){ + g_err(_("Can't allocate memory for new spots")); + return NULL; + } + ptr->spot = malloc(n * sizeof(Spot*)); + if(!ptr->spot){ + g_err(_("Can't allocate memory for pointers in spots array")); + free(ptr); + return NULL; + } + ptr->size = n; + ptr->n = 0; + // originally put the flag BY_COPY, which is removed by the first copy BY_PTR, + // order to do not try to free one area several times + ptr->memflag = BY_COPY; + return ptr; +} + +void spots_free(Spots **spots){ + FNAME(); + int i, n; + if(!(*spots)) return; + n = (*spots)->n; + Spot **spot = (*spots)->spot; + if((*spots)->memflag != BY_PTR) // check whether spot-regions were allocated + for(i = 0; i < n; i++, spot++) + free(*spot); + free((*spots)->spot); + free(*spots); + *spots = NULL; +} + +/* + * Add point spot into array spots by copy. + * If array is full return NULL + * + */ +Spot* spots_add(Spots *spots, Spot *spot, int copy){ + Spot *ptr = NULL; + if(spots->size <= spots->n){ + g_err(_("Spots array is full, can't place new spot\n")); + return NULL; // there's no enough space + } + if(!spot){ + g_err(_("Zero pointer to spot\n")); + return NULL; + } + if(copy == BY_COPY){ + ptr = malloc(sizeof(Spot)); + if(!ptr){ + g_err(_("Can't allocate memory for new spot")); + return NULL; + } + if(!memcpy(ptr, spot, sizeof(Spot))){ + free(ptr); + g_err(_("Can't copy spot to new one")); + return NULL; + } + } + else if(copy == BY_PTR){ + spots->memflag = BY_PTR; // change flag to not do free() + ptr = spot; + } + spots->spot[spots->n] = ptr; // jam into array + spots->n++; // increment points number + return ptr; +} + +/* + * alignment of the coordinate system spot.c to CS relative to the optical axis, + * approximate calculation of the angular and radial coordinates of each spot in + * the coordinates with respect to the center of gravity of spots (for further identification) + */ +void spots_center(Spots *spots){ + int i, n; + double X = 0., Y = 0., N, x0, y0; + double *xx, *yy, *px, *py; + Spot **spot; + n = spots->n; N = (double) n; + if(n == 0) return; + xx = malloc(n * sizeof(double)); px = xx; + yy = malloc(n * sizeof(double)); py = yy; + spot = spots->spot; + for(i = 0; i < n; i++, px++, py++, spot++){ + //x0 = (*spot)->c.x; y0 = (*spot)->c.y; + x0 = (*spot)->c.x - AxisX; + //y0 = (*spot)->c.y - AxisY; + y0 = AxisY - (*spot)->c.y; // flip into right coordinates + (*spot)->c.x = x0; (*spot)->c.y = y0; + *px = x0; + *py = y0; + X += *px; Y += *py; + } + X /= N; Y /= N; + green("\nspots center: %g, %g", X, Y); + px = xx; py = yy; + spot = spots->spot; + for(i = 0; i < n; i++, px++, py++, spot++){ + x0 = *px - X; y0 = *py - Y; + (*spot)->phi = atan2(y0, x0); + (*spot)->r = sqrt(x0*x0 + y0*y0); + } + free(xx); free(yy); +} + +int cmp_spots_r(const void *spot1, const void *spot2){ + return ((*(Spot**)spot1)->r > (*(Spot**)spot2)->r); +} +void sort_spots_by_r(Spots *spots){ + qsort(spots->spot, spots->n, sizeof(Spot*), cmp_spots_r); +} + +int cmp_spots_phi(const void *spot1, const void *spot2){ + return ((*(Spot**)spot1)->phi > (*(Spot**)spot2)->phi); +} +void sort_spots_by_phi(Spots *spots){ + qsort(spots->spot, spots->n, sizeof(Spot*), cmp_spots_phi); +} + +int cmp_spots_id(const void *spot1, const void *spot2){ + return ((*(Spot**)spot1)->id > (*(Spot**)spot2)->id); +} +void sort_spots_by_id(Spots *spots){ + qsort(spots->spot, spots->n, sizeof(Spot*), cmp_spots_id); +} + + +/* + * Sorting the points in R and phi we find the markers. + * After sorting, id of markers is set to + 100 * . + * The markers are numbered as 100 * + +*/ +void sort_spots(Window *window){ + FNAME(); + Spots *spots = window->spots; + gboolean Prefocal = TRUE; + int i, n; + int step = 0, // a step - goto next group + num = 0, // number of ray or ring + mZero = 0; // id of zero ray + Spot **spot = spots->spot; + Spots *markers = spots_alloc(2); // allocate space for markers + double *angles_pre, *angles_post; // angle coordinate of each ray in "normal" orientation + const int N_RAY_MAX = 2 * N_ANGLES - 1; // max ray number + const int N_RAYS = 2 * N_ANGLES; + const int N_ANGLE_MAX = N_ANGLES - 1; // max line number + double r_aver = 0.; // mid radius of spots + n = spots->n; + // fill the array angles_0 (for obtaining further angle of inclination of hartmanogramm) + angles_pre = calloc(N_ANGLES, sizeof(double)); + angles_post = calloc(N_ANGLES, sizeof(double)); + { + angles_post[0] = M_PI_2 - aStep/2.; + angles_pre[0] = M_PI_2 + aStep/2.; + for(i = 1; i < N_ANGLES; i++){ + angles_pre[i] = angles_pre[i-1] + aStep; + angles_post[i] = angles_post[i-1] - aStep; + green("angles[%d]: pre=%g, post=%g", + i, angles_pre[i]*180/M_PI, angles_post[i]*180/M_PI); + } + } + // find the center of gravity of points on hartmanogramm and determine + // for each point the polar coordinates in the system of the center of gravity + spots_center(spots); + // sort points by phi to find markers and number by rays + { + sort_spots_by_phi(spots); + double phi0 = (*spot)->phi, phi, dphi; + green("\nSort by phi\n"); + for(i = 0; i < n; i++, spot++){ + phi = (*spot)->phi; + dphi = fabs(phi-phi0); + phi0 = phi; + // If we two consecutive jump occurs, the previous point has to be a marker. + // If we we find two more markers, generate an error + if(dphi > PHI_STEP){ + num++; + if(!step){ + step = 1; + }else{ + num--; + red("FOUND MARKER"); + (*(spot-1))->id = 99 - (*(spot-1))->id; + DBG("spot[%d]: r=%g, phi=%g\n", (*(spot-1))->id, + (*(spot-1))->r, (*(spot-1))->phi); + if(!spots_add(markers, *(spot-1), BY_PTR)){ + bad_circumstances(TOO_MANY_MARKERS); + goto ret; + } + } + } + else step = 0; + (*spot)->id = num; + //DBG("spot[%d]: r=%g, phi=%g, dphi=%g\n", (*spot)->id, (*spot)->r, + // phi, dphi); + } + } + // check markers + { + if(markers->n != 2){ + if(markers->n == 0){ + bad_circumstances(NOT_ENOUGH_MARKERS); + goto ret; // no marker found + } + spot = spots->spot; + for(i = 0; i < n; i++, spot++) + if((*spot)->id < N_RAYS) r_aver += (*spot)->r; + r_aver /= (double)n; + green("R_aver = %.2f, R=%.2f", r_aver, markers->spot[0]->r); + if(window->image->val_f < 0.){ + // obtain the value of Prefocal from human as VAL_F is unknown + get_prefocal(window, &Prefocal); + }else Prefocal = (window->image->val_f < 100.) ? TRUE : FALSE; + int m_id = markers->spot[0]->id; + if(!Prefocal){ + if(markers->spot[0]->r < r_aver){ // nearest, zero marker + mZero = 99 - m_id; + }else{ // far, fifth marker + mZero = 104 - m_id; // 5 rays clockwise + } + }else{ + if(markers->spot[0]->r < r_aver){ + mZero = 98 - m_id; + }else{ + mZero = 93 - m_id; + } + } + if(markers->spot[0]->r < r_aver) + markers->spot[0]->id = 299; // finally change markers' numbers + else + markers->spot[0]->id = 794; + if(mZero > N_RAY_MAX) mZero -= N_RAYS; + else if(mZero < 0) mZero += N_RAYS; + }else{ + sort_spots_by_r(markers); + spot = markers->spot; + for(i = 0; i < 2; i++) + green("marker #%d: r=%g, phi=%g, tmp_id=%d, x=%g, y=%g", i, + spot[i]->r, spot[i]->phi, spot[i]->id, spot[i]->c.x, spot[i]->c.y); + double dphi = spot[0]->phi - spot[1]->phi; + DBG("Angle between markers: %g degr\n", dphi*180./M_PI); + if(fabs(dphi - DPHI_MARKERS) > PHI_STEP){ + bad_circumstances(BAD_MARKERS_ANGLE); + goto ret; + } + if(dphi > 0.){ + Prefocal = FALSE; + mZero = 99 - spot[0]->id; // number of ray to be zero + }else{ + mZero = 98 - spot[0]->id; // number of ray to be zero + if(mZero > N_RAY_MAX) mZero -= N_RAYS; + } + spot[0]->id = 299; + spot[1]->id = 794; + } + } + // renumbers points in a clockwise direction, starting from the scheme + // EEERRRRRRROOOOOOORRRRR!!!! + // смены направления отсчетов нет: и на пред- и на зафокальном одинаково! + // The numbering starts from zero! + spot = spots->spot; + green("\nmZero = %d\n", mZero); + if(!Prefocal){ // postfocal image + red("\nPostfocal image\n"); + for(i = 0; i < n; i++, spot++){ + if((*spot)->id > N_RAY_MAX) continue; // don't convert markers' numbers + int sid = mZero - (*spot)->id; // change the numbering and its direction + if(sid < 0) sid += N_RAYS; + (*spot)->id = sid; + } + }else{ // prefocal image + red("\nPrefocal image\n"); + for(i = 0; i < n; i++, spot++){ + if((*spot)->id > N_RAY_MAX) continue; + int sid = (*spot)->id - mZero; // only change the numbering + if(sid < 0) sid += N_RAYS; + (*spot)->id = sid; + } + } + /* by the method of least squares we are counting angle of inclination + * y=a+bx, b=tg(slope+alpha_i0) + * b = [\sum x_i \sum y_i - n\sum (x_i y_i)] / + * [ (\sum x_i)^2 - n\sum (x_i)^2 ] + * + * Let S1 = \sum x_i, S2 = \sum y_i, S3 = \sum(x_i)^2, + * S4 = \sum(x_i y_i), then + * b = (S1*S2 - n*S4) / (S1*S1 - n*S3) + * + * Set up arrays four elements Sj of 16 (for each line by an item) + * In the loop over all spots (except markers) fill the array. + * Then we are counting the coefficients b and fill the matrix of slope angles + */ + { + double *S1, *S2, *S3, *S4, *N, *angles; + double x, y, *angles_0; + int idx; + angles_0 = (Prefocal) ? angles_pre : angles_post; + S1 = calloc(N_ANGLES, sizeof(double)); + S2 = calloc(N_ANGLES, sizeof(double)); + S3 = calloc(N_ANGLES, sizeof(double)); + S4 = calloc(N_ANGLES, sizeof(double)); + N = calloc(N_ANGLES, sizeof(double)); + angles = calloc(N_ANGLES, sizeof(double)); + SlopeAngle = 0.; + spot = spots->spot; + for(i = 0; i < n; i++, spot++){ + idx = (*spot)->id; + if(idx > N_RAY_MAX) continue; + if(idx > N_ANGLE_MAX) idx -= N_ANGLES; // convert the ray number to the number of line + x = (*spot)->c.x; y = (*spot)->c.y; + S1[idx] += x; // \sum x_i + S2[idx] += y; // \sum y_i + S3[idx] += x*x; // \sum (x_i)^2 + S4[idx] += x*y; // \sum (x_i y_i) + N[idx] += 1.; + } + for(i = 0; i < N_ANGLES; i++){ + double ang; + // calculate an shift for i-th line + angles[i] = atan2((S1[i]*S2[i] - N[i]*S4[i]) , + (S1[i]*S1[i] - N[i]*S3[i])); + DBG("Angle: %g", angles[i]*180./M_PI); + // check whether there's no jumps near +-pi + if(i){ + ang = angles[i] - angles[i-1]; + DBG("Ang: %g", ang*180./M_PI); + if(!Prefocal){ + if(ang > M_PI) angles[i] -= M_2PI; + else if(ang > 0.) angles[i] -= M_PI; + else if(ang < -M_2PI) angles[i] += M_2PI; + else if(ang < -M_PI) angles[i] += M_PI; + }else{ + if(ang < -M_PI) angles[i] += M_2PI; + else if(ang < 0.) angles[i] += M_PI; + else if(ang > M_2PI) angles[i] -= M_2PI; + else if(ang > M_PI) angles[i] -= M_PI; + } + } + ang = angles[i] - angles_0[i]; + if(ang > M_2PI) ang -= M_2PI; + else if(ang < 0.) ang += M_2PI; + SlopeAngle += ang; + DBG("angle[%d] = %g; ang=%g; a0=%g\n", i, angles[i]*180./M_PI, + ang*180./M_PI, angles_0[i]*180./M_PI); + } + SlopeAngle /= (double)N_ANGLES; // average + red("Slope Angle: %g degr", SlopeAngle * 180. / M_PI); + free(S1); free(S2); free(S3); free(S4); free(N); free(angles); + } + // sort spots by r to enumerate by rings + { + sort_spots_by_r(spots); + spot = spots->spot; + double r0 = (*spot)->r, r; + // rtres == 1/7 of average distance between rings + double rtres = (spot[n-1]->r - r0) / ((double)N_RINGS - 1.) / 7.; + num = 0; + green("\nSort by r\n"); + for(i = 0; i < n; i++, spot++){ + if((*spot)->id > N_RAY_MAX) continue; // we have already counts markers + r = (*spot)->r; + if(fabs(r-r0) > rtres){ + num+=100; + } + (*spot)->id += num; + // DBG("spot[%d]: r=%g, phi=%g, dr=%g\n", (*spot)->id, r, + // (*spot)->phi, r - r0); + r0 = r; + } + if((num/100 + 1) > N_RINGS) + bad_circumstances(TOO_MANY_RINGS); + } + /* + * At this stage it would be nice to display the picture boxes, + * numbered points, detected angle and center. + */ + + /* + * Recalculate the the coordinates of centers points' for the translation + * to the coordinate system relative to the optical axis + * (rotation + reflection from the vertical axis). + * (\beta == SlopeAngle) + * x^0_i = -x_i\cos\beta - y_i\sin\beta; + * y^0_i = -x_i\sin\beta + y_i\cos\beta + */ + { + double cosSA = cos(SlopeAngle); + double sinSA = sin(SlopeAngle); + double x,y; + spot = spots->spot; + if(!Prefocal){ + for(i = 0; i < n; i++, spot++){ + x = (*spot)->c.x; y = (*spot)->c.y; + (*spot)->xC = -x*cosSA - y*sinSA; + (*spot)->yC = -x*sinSA + y*cosSA; + } + }else{ + for(i = 0; i < n; i++, spot++){ + x = (*spot)->c.x; y = (*spot)->c.y; + (*spot)->xC = x*cosSA + y*sinSA; + (*spot)->yC = -x*sinSA + y*cosSA; + } + } + } +ret: + free(angles_pre); free(angles_post); +} + +void spots_save(Spots *spots, gchar *filename){ + Spot **spot, *s; + FILE *f; + if(! spots || ! spots->spot || ! filename) return; + int i, n = spots->n; + sort_spots_by_id(spots); + spot = spots->spot; + f = fopen(filename, "w"); + if(!f){ + g_err(_("Can't open file")); + return; + } + fprintf(f, "\t***** Bounding boxes *****\t** Coords w/o rot. comp. **\tCoords with rot. comp.\n"); + fprintf(f, "id\tx0\ty0\tw\th\txc\tyc\tw2\th2\txc\tyc\n"); + for(i = 0; i < n; i++, spot++){ + s = *spot; + if(fprintf(f, "%03d\t%d\t%d\t%d\t%d\t%.2f\t%.2f\t%.2f\t%.2f\t%.2f\t%.2f\n", + s->id, + s->box.x, s->box.y, s->box.w, s->box.h, + s->c.x, s->c.y, s->c.rx, s->c.ry, + s->xC, s->yC) <= 0){ + g_err(_("Can't write to file")); + break; + } + } + fclose(f); +} + +#else // LEPTONICA_FOUND not set +Spots* spots_alloc(int n __attribute__((unused))){ + LEPTERR; + return NULL; +} +Spot* spots_add(Spots *spots __attribute__((unused)), + Spot *spot __attribute__((unused)), int copy __attribute__((unused))){ + LEPTERR; + return NULL; +} +void spots_free(Spots **spots __attribute__((unused))){ + LEPTERR; +} +void sort_spots(Spots *spots __attribute__((unused))){ + LEPTERR; +} +#endif // LEPTONICA_FOUND diff --git a/src/terrain.c b/src/terrain.c new file mode 100644 index 0000000..0615c09 --- /dev/null +++ b/src/terrain.c @@ -0,0 +1,423 @@ +// terrain.c - functions for 3D view +// +// Copyright 2011 Edward V. Emelianoff +// +// This program 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 2 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 this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +// MA 02110-1301, USA. +#include "opengl.h" +#include "terrain.h" +#include "contours.h" +#include "fits.h" +#include "imtools.h" // for destroy_image + + +#define MINSZ 5 // minimal size of a selection region + +/* + * Calculate the normals for the point (x, y) of the image. + * First, fill arrays of normals for the the lower right and upper + * left triangles with one vertex at a given point. + * Then for each point the total normal is calculated + * + * Principle + * *---B---*---* (rd is the lower right triangle, ul is upper left) + * | / | / | / | Normal at the point A at the map of triangles (as shown) + * E---A---C---* is composed of normal Aul + Bdr + Cul + Adr + Dul + Edr + * | / | / | / | The normals for individual triangles (X - intensity at a point): + * *---D---*---* (normal = vertical vector x horizontal vector) + * Not normalized Aul = (E-A, A-B, 1) + * Adr = (A-C, D-A, 1) + */ + +/* + * Computing of the normals UL & DR + * REWRITE FOR CUDA + */ +GLfloat *mkTrNormals(IMAGE *image){ + FNAME(); + int W = image->width, H = image->height, x, y, i; + /* + * Normal for the the lower right and upper left triangles coming out of a given point + * Component 0 - UL, 1 - DR + * To simplify further calculations, supplemented by a frame in one node (zeros) + */ + GLfloat *tr_norms = calloc((W+2)*(H+2), 6*sizeof(GLfloat)); + int Wstride = (W + 2) * 6; + int ww = W - 1, hh = H - 1; + GLfloat *heights = image->data; + GLfloat *ptr; + #ifdef EBUG + double t0 = dtime(); + #endif + inline void soft_normalize(GLfloat *v){ + GLfloat l,x,y; + x = v[0]; + y = v[1]; + l = sqrtf((x*x) + (y*y) + 1.f); + v[0] = x / l; + v[1] = y / l; + v[2] = 1.f/ l; + } + inline void calc_UL(GLfloat *height, GLfloat *ptr){ + GLfloat A = *height; + if(A){}; + ptr[0] = height[-1] - A; // E - A + ptr[1] = height[-W] - A; // B - A + ptr[2] = 1.f; + soft_normalize(ptr); + } + inline void calc_DR(GLfloat *height, GLfloat *ptr){ + GLfloat A = *height; + if(A){}; + ptr += 3; // move to DR + ptr[0] = A - height[1]; // A - C + ptr[1] = A - height[W]; // A - D + ptr[2] = 1.f; + soft_normalize(ptr); + } + inline void calc_two_norms(GLfloat *height, GLfloat *ptr){ + calc_UL(height, ptr); + calc_DR(height, ptr); + } + ptr = &tr_norms[Wstride + 1]; + #pragma omp parallel for + for(x = 0; x < ww; x++){ + calc_DR(heights+x, ptr + 6*x); // upper border without right point, only DR + } + i = 0; + #pragma omp parallel for private(y, ptr, x, i) + for(y = 1; y < hh; y++){ + ptr = &tr_norms[Wstride*(y+1)+1]; + i = y * W; + calc_DR(heights+(i++), ptr); // left border - only DR + ptr += 6; + for(x = 1; x < ww; x++, ptr += 6){ + calc_two_norms(heights+(i++), ptr); + } + calc_UL(heights+i, ptr); // right border - only UL + } + ptr = &tr_norms[Wstride * H + 1]; + heights = &image->data[hh * W]; + #pragma omp parallel for + for(x = 1; x < W; x++) + calc_UL(heights, ptr+6*x); // down border - onlyUL + /* + ptr = &tr_norms[Wstride + 1]; + for(x = 0; x < ww; x++, ptr += 6, heights++) + calc_DR(heights, ptr); + heights++; + for(y = 1; y < hh; y++){ + ptr = &tr_norms[Wstride*(y+1)+1]; + calc_DR(heights++, ptr); + ptr += 6; + for(x = 1; x < ww; x++, ptr += 6, heights++){ + calc_two_norms(heights, ptr); + } + calc_UL(heights++, ptr); + } + heights++; + ptr = &tr_norms[Wstride * H + 2]; + for(x = 1; x < W; x++, heights++, ptr += 6) + calc_UL(heights, ptr); + */ + DBG("time: %g", dtime() - t0); + return tr_norms; +} + +void computeNormals(Window *window){ + FNAME(); + int x, y; + IMAGE *verts = window->image; + IMAGE *norms = window->image_transformed; + int W = verts->width, H = verts->height; + int Wstride = (W + 2) * 6; + #ifdef EBUG + double t0 = dtime(); + #endif + GLfloat *tr_norms = mkTrNormals(verts); + GLfloat *ptr; + GLfloat *normal = norms->data; + inline void addVectors(GLfloat *norm, GLfloat *dbl){ + GLfloat *Aul, *Adr, *Bdr, *Cul, *Dul, *Edr; + Aul = dbl; Adr = dbl + 3; + Bdr = dbl - Wstride + 3; + Cul = dbl + 6; + Dul = dbl + Wstride; + Edr = dbl - 3; + *norm++ = (*Aul++) + (*Bdr++) + (*Cul++) + (*Adr++) + (*Dul++) + (*Edr++); + *norm++ = (*Aul++) + (*Bdr++) + (*Cul++) + (*Adr++) + (*Dul++) + (*Edr++); + *norm = (*Aul) + (*Bdr) + (*Cul) + (*Adr) + (*Dul) + (*Edr); + } + inline void normalize(GLfloat *v){ + GLfloat l,x,y,z; + x = v[0]; + y = v[1]; + z = v[2]; + l = sqrtf((x*x) + (y*y) + (z*z)); + v[0] = x / l; + v[1] = y / l; + v[2] = z / l; + } + /* + int WW = W*3; + #pragma omp parallel for private(y, ptr, normal) + for(y = 0; y < H; y++){ + ptr = &tr_norms[Wstride*(y+1)+1]; + normal = &norms->data[y*WW]; + for(x = 0; x < W; x++, normal+=3, ptr+=6){ + addVectors(normal, ptr); + normalize(normal); + } + }*/ + for(y = 0; y < H; y++){ + ptr = &tr_norms[Wstride*(y+1)+1]; + for(x = 0; x < W; x++, normal+=3, ptr+=6){ + addVectors(normal, ptr); + normalize(normal); + } + } + free(tr_norms); + DBG("time: %g", dtime() - t0); +} + +/* + * A simple 3D-display selected in window->parent field + * Image corners coordinates (x0, y0), (x, y) + * Coordinates in CS IMAGES, ANGLES are outermost, not known + * window - a window with the 3D image + * X0, y0, x, y - the image area of the parent window, from which information will be taken + * from_file - TRUE to load the file into the same window (then the coordinates do not care) + * FALSE for download from the [sub] the image of the parent window + */ +gboolean loadTerrain(Window *window, + double x0, double y0, double x, double y, + gboolean from_file){ + FNAME(); + int i, j, imW; + IMAGE *verts, *norms; // vertexes & normals (pointers to w->im, w->im_tr) + IMAGE *image = NULL; // pointer to the parent image or a new image + int X0,Y0, X1,Y1; // here will be stored the coordinates of the upper left and lower right corners + int W, H; // Here are the dimensions of the selected area + GLfloat *in, *out; // Pointers to the points in the image + GLfloat scale; // scale (1/8 of the perimeter of selected region) + #ifdef EBUG + double t0 = dtime(); + #endif + if(!from_file){ + if(x0 < x){ X0 = (int)(x0+0.5); X1 = (int)(x+0.5);} + else{ X0 = (int)(x+0.5); X1 = (int)(x0+0.5);} + if(y0 < y){ Y0 = (int)(y0+0.5); Y1 = (int)(y+0.5);} + else{ Y0 = (int)(y+0.5); Y1 = (int)(y0+0.5);} + W = X1 - X0; H = Y1 - Y0; + DBG("subimage size: %dx%d", W, H); + DBG("subimage @ (%d,%d)-(%d,%d)", X0, Y0, X1, Y1); + if(W < MINSZ || H < MINSZ){ + g_err(_("Selected area too small")); + return FALSE; + } + image = window->parent->image; + if(!copy_contours(image, window->image, + (float)-X0, (float)-Y0)) + g_err(_("Error occured when tried to add contours")); + window->image->cLevels = image->cLevels; + + window->image->stat.min = image->stat.min; + window->image->stat.max = image->stat.max; + }else{ // load to the same window from file + gchar *filename; + INIT(image, IMAGE); + if( !(filename = get_open_filename(window)) || + !readfits(filename, image)){ + g_free(filename); + destroy_image(image); + return FALSE; + } + g_free(filename); + X1 = W = image->width; + Y1 = H = image->height; + X0 = Y0 = 0; + } + // check if there are shoals with the filling of the structure of points and normals + free(window->image->data); window->image->data = NULL; + free(window->image_transformed->data); window->image_transformed->data = NULL; + if(!image || !image->data){ + g_err(_("No image in parent window")); + goto abnormal_exit; + } + imW = image->width; + verts = window->image; + norms = window->image_transformed; + verts->data = calloc(W*H, sizeof(GLfloat)); + if(!verts->data){ + g_err(_("Can't allocate memory")); + goto abnormal_exit; + } + // normals for each point + norms->data = calloc(W*H*3, sizeof(GLfloat)); + if(!norms->data){ + g_err(_("Can't allocate memory")); + free(verts->data); + verts->data = NULL; + goto abnormal_exit; + } + verts->width = W; verts->height = H; + norms->width = W; norms->height = H; + scale = ((GLfloat)W + (GLfloat)H) / 4.f / (image->stat.max-image->stat.min); + // Copy the selected piece of image with scaling + #pragma omp parallel for + for(j = Y0; j < Y1; j++){ + //int l = (j - Y0)*W; + in = &image->data[j * imW + X0]; + out = &verts->data[(j - Y0)*W]; + for(i = 0; i < W; i++) + (*out++) = (*in++) * scale; + } + if(from_file) + destroy_image(image); + DBG("time: %g", dtime() - t0); + return TRUE; +abnormal_exit: + if(from_file) + destroy_image(image); + return FALSE; +} + +gboolean createList(Window *window){ + FNAME(); + TEXTURE *texture = window->texture; // number of list stores in texture->tex + GLfloat *Verts = window->image->data; + GLfloat *Norms = window->image_transformed->data; + int W = window->image->width, H = window->image->height; + int x, y, stride = 3 * W; + GLfloat dX = -(GLfloat)W / 2.f, dY = -(GLfloat)H / 2.f; + GtkWidget *Area = window->drawingArea; + if(!Area){ + g_err("???"); + return FALSE; + } + GdkGLContext *glContext = gtk_widget_get_gl_context(Area); + GdkGLDrawable *glDrawable = gtk_widget_get_gl_drawable(Area); + if(!gdk_gl_drawable_gl_begin(glDrawable, glContext)) + g_assert_not_reached(); + #ifdef EBUG + double t0 = dtime(); + #endif + if(!texture){ + INIT(window->texture, TEXTURE); + texture = window->texture; + if(!texture){ + g_err(_("Can't allocate memory for texture")); + return FALSE; + } + if(useVBO){ + glGenBuffersARB(1, &texture->tex); + glGenBuffersARB(1, &texture->w); + }else{ + texture->tex = glGenLists(1); + texture->w = W; + } + if(texture->tex == 0){ + g_err(_("Can't generate VBO / GL list")); + return FALSE; + } + } + texture->h = H; + if(useVBO){ + GLint sz = W*H*3*sizeof(GLfloat), szi = W*(H-1)*2*sizeof(GLuint); + glBindBufferARB(GL_ARRAY_BUFFER_ARB, texture->tex); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, texture->w); + glBufferDataARB(GL_ARRAY_BUFFER_ARB, sz*2, + 0, GL_STATIC_DRAW_ARB); + GLfloat *vertexes = malloc(sz); + GLuint *indexes = malloc(szi); + GLfloat *ptr = vertexes; + #pragma omp parallel for + for(y = 0; y < H; y++){ + ptr = &vertexes[y*W*3]; + Verts = &window->image->data[y*W]; + for(x = 0; x < W; x++, Verts++){ + *ptr++ = dX + x; + *ptr++ = dY + y; + *ptr++ = *Verts; + } + } + H--; + #pragma omp parallel for + for(y = 0; y < H; y++){ + GLuint *iptr = &indexes[y*W*2]; + GLuint idx = y*W; + for(int x = 0; x < W; x++, idx++){ + *iptr++ = idx + W; + *iptr++ = idx; + } + } + glBufferDataARB(GL_ELEMENT_ARRAY_BUFFER, szi, 0, GL_STATIC_DRAW_ARB); + glBufferSubDataARB(GL_ELEMENT_ARRAY_BUFFER, 0, szi, indexes); + glBufferSubDataARB(GL_ARRAY_BUFFER_ARB, 0, sz, vertexes); + glBufferSubDataARB(GL_ARRAY_BUFFER_ARB, sz, + sz, window->image_transformed->data); + free(vertexes); + free(indexes); + free(window->image->data); + window->image->data = NULL; + free(window->image_transformed->data); + window->image_transformed->data = NULL; + //~ free(window->image_transformed); window->image_transformed = NULL; + + }else{ + glNewList(texture->tex, GL_COMPILE); + H--; + for(y = 0 ; y < H; y++){ + glBegin(GL_TRIANGLE_STRIP); + for(x = 0; x < W; x++, Verts++, Norms+=3){ + glNormal3fv(Norms); + glVertex3f(dX + x, dY + y, *Verts); + glNormal3fv(&Norms[stride]); + glVertex3f(dX + x, dY + (y + 1), Verts[W]); + } + glEnd(); + } + glEndList(); + } + gdk_gl_drawable_gl_end(glDrawable); + DBG("time: %g", dtime() - t0); + return TRUE; +} + +/* + * External function for the preparation of [[section] [parental]] picture + * to download into 3D-box + * window - 3D-window + * x0, y0, x, y - the corners of the selected area from picture of the parent window + * from_file - TRUE to load from the called file selection window + * FALSE for download from the parent window + * If from_file == TRUE, the coordinates of the corners of subimage are ignored + */ +void terrain_3D(Window *window, + double x0, double y0, double x, double y, + gboolean from_file){ + FNAME(); + if(!window) return; + if(!loadTerrain(window, x0, y0, x, y, from_file)){ + if(!from_file) destroy_window(window); + return; + } + computeNormals(window); + if(!createList(window)){ + destroy_window(window); + return; + } + init3D(window); // initialisation of openGL window +} diff --git a/src/tracking.c b/src/tracking.c new file mode 100644 index 0000000..5103f3f --- /dev/null +++ b/src/tracking.c @@ -0,0 +1,565 @@ +// tracking.c - funcions for cutting tracking +// +// Copyright 2011 Edward V. Emelianoff +// +// This program 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 2 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 this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +// MA 02110-1301, USA. +#include "tracking.h" +#include "imtools.h" +#include "gauss.h" +#include "opengl.h" + +const double LHWD = 5.; // length of the cross-mark bar +double x_1, y_1, x_2, y_2; // coordinates the selection frame or line +//double GW, GH; // height and width of the graph + +double gY0, gYm, gXm; // range of values of Y, the maximum value of X +double gYmax; // limits to indicate the coordinates in the status bar +double xScale, yScale; // scale by axes +double bot_X = -1., top_X = -1.; // values (in units of X) of the lower and upper bounds on the abscissa +gint g_botX, g_topX; // the same, but in the coordinates of the window + +/* the data for translation of the x coordinate from Pdata into coordinates x, y of image + * X = x0 + x*cx, Y= y0 + x*cy (cx - the cosine of the angle of the cutting, cy - sine) */ +struct{ + double x0; // starting poing + double y0; + double cx; // cosine of the angle of inclination + double cy; // sine -//- +} Pdata2image; +Points *Pdata = NULL; // array of data points +GdkPoint *gPoints = NULL; // points in window coordinates +Points selection = {0, NULL}; // the same for the selected region +GdkPoint *selPoints = NULL; + +gint Npts; // number of data points + +Point *alloc_Pdata(int N){ + if(Pdata){ + free(Pdata->data); + Pdata->data = NULL; + } + else + Pdata = malloc(sizeof(Pdata)); + if(!Pdata){ + g_err(_("Can't allocate memory for track points")); + return NULL; + } + Pdata->data = malloc(N*sizeof(Point)); + if(!Pdata->data){ + g_err(_("Can't allocate memory for track points")); + return NULL; + } + return Pdata->data; +} +// returns the coordinates xx and yy on the image (in CS of the window!) +// for the point with coordinate x in the graph +void get_image_crds(double x, double *xx, double *yy){ + *xx = Pdata2image.x0 + x * Pdata2image.cx; + *yy = Pdata2image.y0 + x * Pdata2image.cy; +} + +// reset the sample +void clear_selection(){ + free(selection.data); + selection.data = NULL; + selection.n = 0; +} +/* + * filling an array Pdata based on the values of coordinates initial (X0, Y0) + * and final (X, Y) points + */ +void get_data(double X0, double X, double Y0, double Y, Window *window){ + int n = 0, N; // new array size + IMAGE *image = window->image; + int x0, y0, yi, WD = image->width, HT = image->height; + double x, y, A, xstep, ystep; + Point *ptr; + gboolean Vert = FALSE; + double tgXY, cosXY; + inline void minmax(double amp){ + if(gY0 > amp) gY0 = amp; + else if(gYm < amp) gYm = amp; + } + inline double dist(double dx, double dy){ + return sqrtf(dx*dx + dy*dy); + } + gXm = dist(X-X0, Y-Y0); + N = (int)(gXm + 1.); + if(N < 2) return; + if(!image->data) + return; + if( X0 < 0. || X0 > WD || X < 0. || X > WD || + Y0 < 0. || Y0 > HT || Y < 0. || Y > HT) return; + if(!(ptr = alloc_Pdata(N))) return; + Pdata2image.x0 = X0; + Pdata2image.y0 = Y0; //HT - 1. - Y0; + Pdata2image.cx = (X-X0)/gXm; + Pdata2image.cy = (Y-Y0)/gXm; + //DBG("N=%d, ", N); + x0 = (int)(X0 + 0.5); + y0 = (int)(Y0 + 0.5); + if(fabs(X-X0) < fabs(Y-Y0)){ // closer to the vertical + Vert = TRUE; + tgXY = (X-X0) / (Y-Y0); + cosXY = fabs(cos(atan(tgXY))); + } + else{ + tgXY = (Y-Y0) / (X-X0); + cosXY = fabs(cos(atan(tgXY))); + } + yi = x0 + WD * y0; + gY0 = gYm = image->data[yi];// * wd + min; + if(Vert){ + ystep = (Y > Y0) ? cosXY : -cosXY; + for(y = Y0; ; y+=ystep, n++, ptr++){ + if(n > N) break; + if(ystep > 0.){ + if(y > Y) break; + } + else{ + if(y < Y) break; + } + ptr->x = (double)n; + x = X0 + tgXY * (y - Y0); + //yi = (int)(x+0.5) + WD * (HT - (int)(y+0.5)); + yi = (int)(x+0.5) + WD * ((int)(y+0.5)); + A = image->data[yi];// * wd + min; + ptr->y = A; + minmax(A); + } + } + else{ + xstep = (X > X0)? cosXY : -cosXY; + for(x = X0; ; x+=xstep, n++, ptr++){ + if(n > N) break; + if(xstep > 0.){ + if(x > X) break; + } + else{ + if(x < X) break; + } + y = Y0 + tgXY * (x-X0); + ptr->x = (double)n; + //yi = (int)(x+0.5) + WD * (HT - (int)(y+0.5)); + yi = (int)(x+0.5) + WD * ((int)(y+0.5)); + A = image->data[yi];// * wd + min; + ptr->y = A; + minmax(A); + } + } + //DBG("Ymin=%g, Ymax=%g\n", gY0, gYm); + Pdata->n = n - 1; +} + +void draw_cross(GdkDrawable *d, GdkGC *gc, double x, double y){ + gdk_draw_line(d, gc, x-LHWD,y, x+LHWD, y); + gdk_draw_line(d, gc, x,y-LHWD, x, y+LHWD); +} +// draw rectangle (gdk_draw_rectangle don't match because it require x,y > x0,y0 +void draw_rect(GdkDrawable *d, GdkGC *gc, double x0, double y0, double x, double y){ + gdk_draw_line(d, gc, x0,y0, x0,y); + gdk_draw_line(d, gc, x0,y0, x,y0); + gdk_draw_line(d, gc, x,y, x, y0); + gdk_draw_line(d, gc, x,y, x0, y); +} + +// get coordinates of selected region +void get_sel_region(double *x0, double *y0, double *x, double *y){ + *x0 = x_1; *y0 = y_1; *x = x_2; *y = y_2; +} +void do_tracking(double xx, double yy, gboolean start, Window *window){ + static double xold, yold, X0, Y0; + double X, Y; + double imW = (double)window->texture->w; + double imH = (double)window->texture->h; + Context *c = window->context; + gboolean conv = FALSE; + x_2 = xx; y_2 = yy; + conv_mouse_to_image_coords(x_2, y_2, &X, &Y, window); + //FNAME(); + if(!window->image->data) return; + if(X > imW){ X = imW; conv = TRUE;} + else if(X < 0){ X = 0; conv = TRUE;} + if(Y > imH){ Y = imH; conv = TRUE;} + else if(Y < 0){ Y = 0; conv = TRUE;} + if(conv) conv_image_to_mouse_coords(X, Y, &x_2, &y_2, window); + //DBG("X=%g, Y=%g, x=%g, y=%g", X, Y, x_2, y_2); + if(start){ + clear_selection(); + X0 = X; Y0 = Y; + x_1 = xold = x_2; + y_1 = yold = y_2; + return; + } + if(c->trackingMode == TRACK_VERT){ + X = X0; x_2 = x_1; + } + else if(c->trackingMode == TRACK_HORIZ){ + Y = Y0; y_2 = y_1; + } + else if(c->trackingMode == TRACK_DIAG){ + double s, adx, ady; + gboolean changed = FALSE; + adx = fabs(X-X0); ady = fabs(Y-Y0); + void corrY(){ + s = (Y > Y0) ? 1. : -1.; + Y = Y0 + s * adx; + y_2 = y_1 + s * fabs(x_2-x_1); + if(Y > imH || Y < 0.){ + if(Y > imH) Y = imH; + else Y = 0.; + conv_image_to_mouse_coords(X, Y, &x_2, &y_2, window); + changed = TRUE; + } + /* + if(Y > imH){ Y = imH; y = Y/Daspect + Dy; changed = TRUE;} + else if(Y < 0){ Y = 0; y = Dy; changed = TRUE;}*/ + ady = fabs(Y-Y0); + //DBG("Y=%g, Y0=%g", Y, Y0); + } + void corrX(){ + s = (X > X0) ? 1. : -1; + X = X0 + s * ady; + x_2 = x_1 + s * fabs(y_2-y_1); + if(X > imW || X < 0.){ + if(X > imW) X = imW; + else X = 0.; + conv_image_to_mouse_coords(X, Y, &x_2, &y_2, window); + changed = TRUE; + } + adx = fabs(X-X0); + //DBG("X=%g, X0=%g", X, X0); + } + if(adx > ady){ + corrY(); + if(changed) corrX(); + } + else{ + corrX(); + if(changed) corrY(); + } + } + + GdkDrawable *d = window->drawingArea->window; + static GdkGC *gc = NULL; + if(!gc){ + gc = gdk_gc_new(d); + gdk_gc_set_foreground(gc, &(window->drawingArea->style->white)); + gdk_gc_set_function(gc, GDK_XOR); + } + if(c->drawingMode == DRAW_QUAD){// draw rectangle + draw_rect(d, gc, x_1,y_1, xold,yold); + draw_rect(d, gc, x_1,y_1, x_2,y_2); + } + else{ // draw lint + // first erase old one + gdk_draw_line(d, gc, x_1,y_1, xold,yold); + draw_cross(d, gc, xold, yold); + // then draw new one + gdk_draw_line(d, gc, x_1,y_1, x_2,y_2); + draw_cross(d, gc, x_2, y_2); + //DBG("Xima=%g, Yima=%g (X0=%g, Y0=%g)", X, Y, X0, Y0); + get_data(X0, X, Y0, Y, window); + bot_X = 0.; top_X = gXm; + //DBG("X=%g, X0=%g, Y=%g, Y0=%g", X, X0, Y, Y0); + gchar str[80]; + g_snprintf(str, 79, "length=%.1f, angle=%.1fdegr", + sqrt((X-X0)*(X-X0)+(Y-Y0)*(Y-Y0)), + atan2(Y-Y0,X-X0)/M_PI*180.); + set_status_text(StatusText, str, window->parent); + set_status_text(StatusText, "", window); + Gconfigure(window->graphWindow); + gdk_window_invalidate_rect(window->graphWindow->drawingArea->window, + NULL, TRUE); + } + xold = x_2; yold = y_2; +} + +gboolean Gexpose(Window *window){//, GdkEventExpose *event, gpointer userData){ + cairo_t *cr = NULL; + int i; + GtkWidget *Area = window->drawingArea; + int H = (int)(Area->allocation.height + 0.5); + GdkPoint *gptr = &gPoints[1]; + if(!cr){ + cr = gdk_cairo_create(Area->window); + cairo_set_line_width (cr, 1.); + } + cairo_set_source_rgb(cr, 0., 0., 0.); + if(!Pdata || !gPoints) + return FALSE; + if(!Pdata->data || Pdata->n < 2) + return FALSE; + cairo_move_to(cr, gPoints->x, gPoints->y); + for(i = 1; i < Npts; i++, gptr++) + cairo_line_to(cr, gptr->x, gptr->y); + cairo_stroke(cr); + cairo_set_source_rgb(cr, 0., 1., 0.); + cairo_move_to(cr, g_botX, 0); + cairo_line_to(cr, g_botX, H); + cairo_stroke(cr); + cairo_set_source_rgb(cr, 1., 0., 0.); + cairo_move_to(cr, g_topX, 0); + cairo_line_to(cr, g_topX, H); + cairo_stroke(cr); + if(selPoints && selection.n){ + int n = selection.n; + cairo_set_source_rgb(cr, 0., 0., 1.); + cairo_move_to(cr, selPoints->x, selPoints->y); + gptr = &selPoints[1]; + //DBG("n=%d", n); + for(i = 1; i < n; i++, gptr++) + cairo_line_to(cr, gptr->x, gptr->y); + cairo_stroke(cr); + } + cairo_destroy(cr); + return FALSE; +} + +// calculation of the chart points in window coordinates +gboolean Gconfigure(Window *window){//, GdkEventConfigure *event, gpointer none){ + int i, n; + Point *ptr; + //FNAME(); + double W, H, ym, y0; + GdkPoint *gptr; + gboolean log_scale = isYlog(window); + if(!Pdata) + return FALSE; + if(!Pdata->data || Pdata->n < 2) + return FALSE; + GtkWidget *Area = window->drawingArea; + free(gPoints); + gPoints = NULL; + Npts = Pdata->n; + gPoints = malloc((Npts + 1)* sizeof(GdkPoint)); + if(!gPoints){ + g_err(_("Can't allocate memory for track points")); + return FALSE; + } + ptr = Pdata->data; + if(!ptr) return FALSE; + gptr = gPoints; + W = Area->allocation.width; + H = Area->allocation.height; + ym = gYm; y0 = gY0; + if(log_scale){ + if(y0 < 0.) y0 = -log(-y0 + 1.); + else y0 = log(y0 + 1.); + if(ym < 0.) ym = -log(-ym + 1.); + else ym = log(ym + 1.); + } + xScale = W / gXm; + yScale = H / (ym - y0); + gYmax = ym; + for(i = 0; i < Npts; i++, ptr++, gptr++){ + gptr->x = (int)(ptr->x * xScale + 0.5); + if(log_scale){ + double yy = ptr->y; + if(yy < 0.) + yy = -log(-yy + 1.); + else + yy = log(yy + 1.); + gptr->y = (int)(H - (yy - y0) * yScale + 0.5); + } + else + gptr->y = (int)(H - (ptr->y - y0) * yScale + 0.5); + } + g_botX = (int)(bot_X * xScale + 0.5); + g_topX = (int)(top_X * xScale + 0.5); + // Long live code monkeys of all time! + if(selection.data) do{ + n = selection.n; + free(selPoints); + selPoints = malloc((n+1)* sizeof(GdkPoint)); + if(!selPoints) break; + gptr = selPoints; + ptr = selection.data; + for(i = 0; i < n; i++, ptr++, gptr++){ + gptr->x = (int)(ptr->x * xScale + 0.5); + if(log_scale){ + double yy = ptr->y; + if(yy < 0.) + yy = -log(-yy + 1.); + else + yy = log(yy + 1.); + gptr->y = (int)(H - (yy - y0) * yScale + 0.5); + } + else + gptr->y = (int)(H - (ptr->y - y0) * yScale + 0.5); +// DBG("g: (%g, %g), w: (%d, %d)", ptr->x, ptr->y, gptr->x, gptr->y); + } + }while(0); + /* + if(ym > 1000.) + set_Grulers(y0/1000, gXm, ym/1000); + else*/ + set_Grulers(y0, gXm, ym, window); + return FALSE; +} + +// display mode of the histogram in the field of graph +void do_histogram(Window *window){ + FNAME(); + IMAGE *image = window->context->current_image; + if(!image || !image->data) image = window->image; + if(!image || !image->data) return; + int i, hsize = image->stat.histogram.size; + int *hptr = image->stat.histogram.data; + Point *ptr; + double histsiz = (double)image->stat.histogram.size; + double scale = image->stat.histogram.scale * (double)image->stat.histogram.size; + //double imin = image->stat.min; + if(!(ptr = alloc_Pdata(hsize))) return; + gXm = 1.;//scale+imin; + scale /= histsiz; + gYm = (double)(image->stat.histogram.max); + gY0 = (double)(image->stat.histogram.min); + for(i = 0; i < hsize; i++, ptr++, hptr++){ + ptr->x = (double) i / histsiz; + //ptr->x = (double) i * scale + imin; + ptr->y = (double) *hptr; + } + Pdata->n = hsize; + DBG("bot: %g, top: %g", image->stat.histogram.bot_tres, image->stat.histogram.top_tres); + if(image->stat.histogram.bot_tres > 0.) + bot_X = image->stat.histogram.bot_tres; + else + bot_X = 0.; + if(image->stat.histogram.top_tres > 0.) + top_X = image->stat.histogram.top_tres; + else + top_X = gXm; + Gconfigure(window->graphWindow); + gdk_window_invalidate_rect(window->graphWindow->drawingArea->window, NULL, TRUE); +} + +/* + * will try to move boundaries when clicking near graph borders + * when clicked near a curve - something else can be done ... + * Window - graphics window + * Return value: TRUE, if will need to handle mouse motion events further + */ +gboolean graph_mouse_btn(int x, Window *window){ + gboolean redraw = FALSE; + gboolean ret = FALSE; + double W, xScale, tmp; + GtkWidget *graphArea = window->drawingArea; + int dist = (g_topX - g_botX) / 2; // half of a distance between borders + if(dist < PT_TRESHOLD) dist = PT_TRESHOLD; + W = graphArea->allocation.width; + xScale = W / gXm; + if(abs(x - g_botX) < dist || x < g_botX){ // select down border + tmp = (double) x / xScale; + if(tmp < top_X) bot_X = tmp; + if(bot_X < 0.) bot_X = 0.; + ret = TRUE; + redraw = TRUE; + } + else if(abs(x - g_topX) < dist || x > g_topX){ // select up border + tmp = (double) x / xScale; + if(tmp > bot_X) top_X = tmp; + if(top_X > gXm) top_X = gXm; + ret = TRUE; + redraw = TRUE; + } + if(redraw){ + gchar msg[80]; + //DBG("move: botX=%g, topX=%g", bot_X, top_X); + g_snprintf(msg, 79, + _("Selected region from %.1f to %.1f, %d datapoints"), + bot_X, top_X, (int)(((double)Npts)/gXm*(top_X-bot_X)+0.5)); + set_status_text(StatusText, msg, window); + Gconfigure(window); + gdk_window_invalidate_rect(graphArea->window, NULL, TRUE); + } + return ret; +} + +void gen_tres_texture(Window *window){ + window->image->stat.histogram.bot_tres = bot_X; + window->image->stat.histogram.top_tres = top_X; + treshold_image(window, bot_X / gXm, top_X / gXm); +} + +gboolean isYlog(Window *window){ + gboolean log_scale = FALSE; + if(window->context->graphMode == GR_GRAPH){ + if(window->context->graphYaxis & Y_LOGGRAPH) + log_scale = TRUE; + } else if(window->context->graphYaxis & Y_LOGHIST) + log_scale = TRUE; + return log_scale; +} + +gboolean fit_gaussian(Window *window){ + //FNAME(); + Point *pt, *pt0; + double C, A, s, x0; + double xx, yy; + #ifndef GSL_FOUND + GSLERR; + return FALSE; + #endif + if(!window->parent || !window->parent->texture){ + g_err(_("No parent window or image")); + return FALSE; + } + double imW = (double)window->parent->texture->w; + double imH = (double)window->parent->texture->h; + gchar msg[160]; + GtkWidget *graphArea = window->drawingArea; + if(!Pdata){ // no points? (O_o, but such a thing be?) + g_err(_("No data points")); + return FALSE; + } + int n = (int)(((double)Npts)/gXm*(top_X-bot_X) + 0.5); + if(n < 10 || n > (int)(sqrt(imW * imW + imH * imH))){ // few or many points + g_err(_("Bad amount of data points")); + return FALSE; + } + int n0 = (int)(((double)Npts)/gXm*bot_X); // the number of a first point + if(n0 < 0 || (n0 + n) > Npts){ // do not get there? + g_err(_("Invalid number of first point")); + return FALSE; + } + clear_selection(); + selection.n = n; + selection.data = malloc(n * sizeof(Point)); + if(!selection.data){ + g_err(_("Can't allocate memory")); + return FALSE; + } + pt = selection.data; + pt0 = &Pdata->data[n0]; + DBG("pt0: (%g, %g)", pt0->x, pt0->y); + // Copy the desired sample into a separate array + for(; n--; pt++, pt0++){ + pt->x = pt0->x; + pt->y = pt0->y; + } + gauss_fit(&selection, &C, &A, &s, &x0); + gaussian_v(C, A, s, x0, &selection); + get_image_crds(x0, &xx, &yy); + //DBG("move: botX=%g, topX=%g", bot_X, top_X); + g_snprintf(msg, 159, + _("Fit gaussian, x0=%.2f I(%.2f, %.2f), s=%.2f, A=%.1f, C=%.3f"), + x0, xx, yy, s, A, C); + set_status_text(StatusText, msg, window); + Gconfigure(window); + gdk_window_invalidate_rect(graphArea->window, NULL, TRUE); + return TRUE; +} diff --git a/src/ui.h b/src/ui.h new file mode 100644 index 0000000..202090f --- /dev/null +++ b/src/ui.h @@ -0,0 +1,1281 @@ +const gchar *ui = "" +"" +"" +"" +"" +"" +"" +"mainFileMenuItem" +"_File" +"" +"" +"" +"" +"gtk-open" +"mainOpenMenuItem" +"" +"" +"" +"" +"" +"gtk-save-as" +"mainSaveAsMenuItem" +"" +"" +"" +"" +"gtk-open" +"mainOpenInNewMenuItem" +"Open in _new window" +"" +"" +"" +"" +"" +"gtk-open" +"mainOpenIn3DMenuItem" +"Open in _3D window" +"" +"" +"" +"" +"" +"mainQuitMenuItem" +"_Quit" +"" +"" +"" +"" +"mainEditMenuItem" +"_Edit" +"" +"" +"" +"" +"mainViewMenuItem" +"_View" +"" +"" +"" +"" +"mainHistMenuItem" +"Show _histogram" +"" +"" +"" +"" +"" +"mainHeadersMenuItem" +"Show _headers" +"" +"" +"" +"" +"" +"main3DviewFullMenuItem" +"3D view of full image" +"" +"" +"" +"" +"" +"main3DviewFrameMenuItem" +"3D view of subframe" +"" +"" +"" +"" +"" +"mainSpotsidentMenuItem" +"Select _spots" +"" +"" +"" +"" +"" +"mainTrackMenuItem" +"Draw _tracks" +"" +"" +"" +"" +"" +"mainZoomframeMenuItem" +"_Zoom frame" +"" +"" +"" +"" +"" +"mainZoomrestoreMenuItem" +"_Restore image" +"" +"" +"" +"" +"" +"mainZoomnearMenuItem" +"Zoom _in" +"" +"" +"" +"" +"" +"mainZoomfarMenuItem" +"Zoom _out" +"" +"" +"" +"" +"" +"mainMathMenuItem" +"_Math" +"" +"" +"" +"" +"mainSpotsMenuItem" +"Find and enumerate spots" +"_Spots" +"" +"" +"" +"" +"mainSpotsParamMenuItem" +"Choose minimal & maximal spot size" +"Size _tresholds" +"" +"" +"" +"" +"" +"mainFindSpotsMenuItem" +"Identify _spots" +"" +"" +"" +"" +"" +"mainHartmannMenuItem" +"So_rt hartmann spots" +"" +"" +"" +"" +"" +"mainSaveSpotsMenuItem" +"Sa_ve spots" +"" +"" +"" +"" +"" +"mainCirclesMenuItem" +"Identify _circles" +"" +"" +"" +"" +"" +"mainHoughMenuItem" +"_Hough transform" +"" +"" +"" +"" +"" +"mainFilterMenuItem" +"_Filter" +"" +"" +"" +"" +"" +"mainHelpMenuItem" +"_Help" +"" +"" +"" +"" +"gtk-about" +"mainAboutMenuItem" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"graphFileMenuItem" +"_File" +"" +"" +"" +"" +"gtk-close" +"graphCloseMenuItem" +"" +"" +"" +"" +"" +"" +"graphEditMenuItem" +"_Edit" +"" +"" +"" +"" +"graphViewMenuItem" +"_View" +"" +"" +"" +"" +"graphScaleMenuItem" +"Y axis scale" +"" +"" +"" +"" +"True" +"graphLinMenuItem" +"Linear" +"" +"" +"" +"" +"" +"graphLinMenuItem" +"graphLogMenuItem" +"Log" +"" +"" +"" +"" +"" +"graphMathMenuItem" +"_Math" +"" +"" +"" +"" +"graphGaussMenuItem" +"Approximate by _gaussian" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"oglFileMenuItem" +"_File" +"" +"" +"" +"" +"gtk-open" +"oglOpenMenuItem" +"" +"" +"" +"" +"" +"gtk-close" +"oglCloseMenuItem" +"" +"" +"" +"" +"" +"oglEditMenuItem" +"_Edit" +"" +"" +"" +"" +"oglViewMenuItem" +"_View" +"" +"" +"" +"" +"oglMoveMenuItem" +"_Move" +"" +"" +"" +"" +"oglXAplusMenuItem" +"Rotate X CW" +"" +"" +"" +"" +"" +"oglXAminusMenuItem" +"Rotate X CCW" +"" +"" +"" +"" +"" +"oglZAplusMenuItem" +"Rotate Z CCW" +"" +"" +"" +"" +"" +"oglZAminusMenuItem" +"Rotate Z CW" +"" +"" +"" +"" +"" +"oglXplusMenuItem" +"Move right" +"" +"" +"" +"" +"" +"oglXminusMenuItem" +"Move left" +"" +"" +"" +"" +"" +"oglYplusMenuItem" +"Move down" +"" +"" +"" +"" +"" +"oglYminusMenuItem" +"Move up" +"" +"" +"" +"" +"" +"oglZplusMenuItem" +"Move backward" +"" +"" +"" +"" +"" +"oglZminusMenuItem" +"Move forward" +"" +"" +"" +"" +"" +"oglGameModeMenuItem" +"Mouse and arrow keys navigation" +"_Game mode" +"" +"" +"" +"" +"" +"oglZoomrestoreMenuItem" +"_Restore image" +"" +"" +"" +"" +"" +"oglMathMenuItem" +"_Math" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"" +"True" +"vertical" +"" +"" +"True" +"" +"" +"False" +"0" +"" +"" +"" +"" +"" +"" +"" +"True" +"3" +"3" +"" +"" +"400" +"400" +"True" +"" +"" +"1" +"3" +"2" +"" +"" +"" +"" +"True" +"10" +"5" +"10" +"" +"" +"1" +"3" +"2" +"3" +"GTK_EXPAND | GTK_SHRINK | GTK_FILL" +"GTK_FILL" +"" +"" +"" +"" +"True" +"vertical" +"10" +"5" +"10" +"" +"" +"2" +"GTK_FILL" +"GTK_EXPAND | GTK_SHRINK | GTK_FILL" +"" +"" +"" +"" +"" +"" +"" +"2" +"" +"" +"" +"" +"True" +"1" +"" +"" +"True" +"True" +"False" +"False" +"" +"True" +"none" +"" +"" +"0" +"" +"" +"" +"" +"True" +"True" +"False" +"False" +"" +"True" +"none" +"" +"" +"False" +"1" +"" +"" +"" +"" +"True" +"True" +"False" +"False" +"" +"True" +"none" +"" +"" +"False" +"2" +"" +"" +"" +"" +"True" +"True" +"False" +"False" +"" +"True" +"none" +"" +"" +"False" +"3" +"" +"" +"" +"" +"False" +"3" +"" +"" +"" +"" +"" +"" +"" +"" +"True" +"vertical" +"" +"" +"True" +"" +"" +"False" +"0" +"" +"" +"" +"" +"" +"" +"" +"True" +"3" +"3" +"" +"" +"400" +"400" +"True" +"" +"" +"1" +"3" +"2" +"" +"" +"" +"" +"True" +"10" +"5" +"10" +"" +"" +"1" +"3" +"2" +"3" +"GTK_EXPAND | GTK_SHRINK | GTK_FILL" +"GTK_FILL" +"" +"" +"" +"" +"True" +"vertical" +"10" +"5" +"10" +"" +"" +"2" +"GTK_FILL" +"GTK_EXPAND | GTK_SHRINK | GTK_FILL" +"" +"" +"" +"" +"" +"" +"" +"2" +"" +"" +"" +"" +"True" +"" +"" +"True" +"True" +"False" +"False" +"" +"True" +"none" +"" +"" +"0" +"" +"" +"" +"" +"True" +"True" +"False" +"8" +"False" +"" +"8" +"True" +"none" +"" +"" +"False" +"1" +"" +"" +"" +"" +"True" +"True" +"False" +"40" +"False" +"" +"28" +"True" +"none" +"" +"" +"False" +"False" +"end" +"2" +"" +"" +"" +"" +"False" +"3" +"" +"" +"" +"" +"" +"" +"" +"" +"True" +"vertical" +"" +"" +"True" +"" +"" +"False" +"0" +"" +"" +"" +"" +"" +"" +"" +"400" +"400" +"True" +"" +"" +"2" +"" +"" +"" +"" +"True" +"1" +"" +"" +"True" +"True" +"False" +"False" +"" +"True" +"none" +"" +"" +"0" +"" +"" +"" +"" +"True" +"True" +"False" +"False" +"" +"True" +"none" +"" +"" +"False" +"1" +"" +"" +"" +"" +"True" +"True" +"False" +"False" +"" +"True" +"none" +"" +"" +"False" +"2" +"" +"" +"" +"" +"True" +"True" +"False" +"False" +"" +"True" +"none" +"" +"" +"False" +"3" +"" +"" +"" +"" +"False" +"3" +"" +"" +"" +"" +"" +"" +"Spots' size treshold" +"False" +"True" +"center-on-parent" +"True" +"" +"" +"True" +"7" +"7" +"7" +"7" +"" +"" +"True" +"vertical" +"7" +"" +"" +"True" +"7" +"True" +"" +"" +"True" +"0" +"out" +"" +"" +"True" +"12" +"" +"" +"True" +"vertical" +"4" +"True" +"" +"" +"True" +"5" +"" +"" +"True" +"Min" +"" +"" +"False" +"False" +"0" +"" +"" +"" +"" +"True" +"True" +"" +"" +"" +"1" +"" +"" +"" +"" +"False" +"False" +"0" +"" +"" +"" +"" +"True" +"5" +"" +"" +"True" +"Max" +"" +"" +"False" +"False" +"0" +"" +"" +"" +"" +"True" +"True" +"" +"" +"" +"1" +"" +"" +"" +"" +"False" +"False" +"1" +"" +"" +"" +"" +"" +"" +"" +"" +"True" +"<b>Width</b>" +"True" +"" +"" +"" +"" +"0" +"" +"" +"" +"" +"True" +"0" +"in" +"" +"" +"True" +"12" +"" +"" +"True" +"vertical" +"5" +"True" +"" +"" +"True" +"5" +"" +"" +"True" +"Min" +"" +"" +"False" +"False" +"0" +"" +"" +"" +"" +"True" +"True" +"" +"" +"" +"1" +"" +"" +"" +"" +"False" +"False" +"0" +"" +"" +"" +"" +"True" +"5" +"" +"" +"True" +"Max" +"" +"" +"False" +"False" +"0" +"" +"" +"" +"" +"True" +"True" +"" +"" +"" +"1" +"" +"" +"" +"" +"False" +"False" +"1" +"" +"" +"" +"" +"" +"" +"" +"" +"True" +"<b>Height</b>" +"True" +"" +"" +"" +"" +"1" +"" +"" +"" +"" +"0" +"" +"" +"" +"" +"True" +"10" +"True" +"" +"" +"gtk-cancel" +"True" +"True" +"True" +"True" +"" +"" +"False" +"0" +"" +"" +"" +"" +"gtk-ok" +"True" +"True" +"True" +"True" +"" +"" +"False" +"1" +"" +"" +"" +"" +"False" +"False" +"1" +"" +"" +"" +"" +"" +"" +"" +"" +"5" +"normal" +"False" +"" +"" +"True" +"vertical" +"2" +"" +"" +"True" +"7" +"7" +"7" +"" +"" +"True" +"7" +"" +"" +"True" +"0" +"none" +"" +"" +"True" +"12" +"" +"" +"True" +"True" +"" +"" +"" +"" +"" +"" +"" +"True" +"<b>Focus value, mm</b>" +"True" +"" +"" +"" +"" +"0" +"" +"" +"" +"" +"True" +"0" +"none" +"" +"" +"True" +"12" +"" +"" +"True" +"vertical" +"5" +"True" +"" +"" +"Prefocal" +"True" +"True" +"False" +"True" +"True" +"" +"" +"0" +"" +"" +"" +"" +"Postfocal" +"True" +"True" +"False" +"True" +"FocalPreRadioBtn" +"" +"" +"1" +"" +"" +"" +"" +"" +"" +"" +"" +"True" +"<b>Image type</b>" +"True" +"" +"" +"" +"" +"1" +"" +"" +"" +"" +"" +"" +"1" +"" +"" +"" +"" +"True" +"center" +"" +"" +"" +"" +"" +"gtk-ok" +"True" +"True" +"True" +"True" +"" +"" +"False" +"False" +"1" +"" +"" +"" +"" +"False" +"end" +"0" +"" +"" +"" +"" +"" +"" ;